Spring Boot+Keycloak OAuth2授权码流自定义JWT转换器未调用问题
问题分析
从日志可见,当前认证流程生成的是OAuth2AuthenticationToken而非资源服务器的JwtAuthenticationToken——这是因为浏览器访问走的是OAuth2客户端登录流程(基于Session),而非资源服务器的JWT Bearer Token认证流程,因此你配置在oauth2ResourceServer下的自定义JwtAuthConverter不会被触发。这直接导致接口权限校验时,用户的Granted Authorities中没有ROLE_client_admin,返回403错误。
解决方案
需要同时处理两种认证场景的权限转换:浏览器端的OAuth2登录流程,以及API调用的JWT Bearer流程。以下是具体实现:
1. 自定义OAuth2用户服务(处理浏览器登录场景)
创建CustomOAuth2UserService,从Keycloak返回的ID Token中提取角色并添加ROLE_前缀:
@Component public class CustomOAuth2UserService extends DefaultOAuth2UserService { @Value("${jwt.auth.converter.resource-id}") private String resourceId; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User user = super.loadUser(userRequest); Map<String, Object> attributes = user.getAttributes(); List<String> roles = new ArrayList<>(); // 提取Realm级角色 if (attributes.containsKey("realm_access")) { Map<String, Object> realmAccess = (Map<String, Object>) attributes.get("realm_access"); roles.addAll((List<String>) realmAccess.get("roles")); } // 提取客户端级角色 if (attributes.containsKey("resource_access")) { Map<String, Object> resourceAccess = (Map<String, Object>) attributes.get("resource_access"); if (resourceAccess.containsKey(resourceId)) { Map<String, Object> clientRoles = (Map<String, Object>) resourceAccess.get(resourceId); roles.addAll((List<String>) clientRoles.get("roles")); } } // 转换为Spring Security权限格式 Set<GrantedAuthority> authorities = roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toSet()); // 保留原有Scope权限 authorities.addAll(user.getAuthorities()); return new DefaultOAuth2User(authorities, attributes, "preferred_username"); } }
2. 更新SecurityConfig配置
将自定义用户服务绑定到OAuth2登录流程,同时保留资源服务器的JWT转换器配置:
@RequiredArgsConstructor @EnableWebSecurity @EnableMethodSecurity @Configuration public class SecurityConfig { private final JwtAuthConverter jwtAuthConverter; private final CustomOAuth2UserService customOAuth2UserService; @Value(value = "${jwt.auth.converter.issuer-uri}") private String issuerUri; @Bean public SecurityFilterChain clientFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(req-> req .anyRequest().authenticated()) // 配置OAuth2登录的用户信息处理 .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService)) ) // 保留资源服务器的JWT配置(供API调用场景使用) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) ) .build(); } @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withIssuerLocation(issuerUri).build(); } }
3. 完善Keycloak配置
- 在Keycloak客户端设置中,确保
rolesScope已启用; - 进入客户端的"Client Scopes" -> "roles" -> "Mappers",确认角色映射器已配置为将角色添加到ID Token和Access Token中。
排查思路
- 确认认证流程类型:通过日志中的
Authentication类型判断当前走的是客户端登录还是资源服务器JWT流程; - 解析ID Token内容:用jwt.io解析Keycloak返回的ID Token,检查是否包含
realm_access/resource_access字段及目标角色; - 校验组件注册:确认自定义
JwtAuthConverter和CustomOAuth2UserService被Spring容器正确扫描(@Component注解生效); - 检查Keycloak权限配置:验证角色是否被正确分配给用户,且客户端设置中允许角色出现在Token中。
教程推荐
- Spring Security官方文档:重点学习OAuth2 Client的用户信息处理、权限转换,以及Resource Server的JWT配置,掌握双认证模式的整合;
- Keycloak官方文档:Spring Boot整合章节,包含授权码流配置、角色映射、权限控制的详细步骤;
- 手动实现权限转换:通过扩展
DefaultOAuth2UserService或实现OAuth2AuthenticationConverter,自定义从OAuth2用户信息中提取角色并转换为Spring Security权限格式。
内容的提问来源于stack exchange,提问作者Man13
相关产品推荐
相关产品推荐

