Spring Boot集成Keycloak无法获取JWT中携带的用户角色如何解决
问题根因
Spring Security默认的OIDC用户解析逻辑只会提取JWT中scope声明对应的权限(也就是你看到的SCOPE_开头的条目)和默认基础角色,不会主动解析Keycloak存储角色的自定义嵌套声明路径,因此即使JWT本身携带全量角色,框架也不会自动映射到权限集合中。
排查与解决步骤
第一步:确认JWT中角色的实际存储路径
把Postman拿到的JWT解码后逐字段检查,Keycloak签发的JWT角色通常存在两个位置:- 域级角色:
realm_access.roles数组 - 客户端级角色:
resource_access.{你的客户端ID}.roles数组
先记好角色所在的完整JSON路径,后续配置要严格对应这个路径,路径错误是最常见的配置失败原因。
注意:如果解码JWT后根本找不到对应角色数组,先去Keycloak后台检查客户端配置:确认给用户/组分配了对应角色,且客户端开启了对应角色的全范围允许(Full Scope Allowed),否则Keycloak不会把角色写入签发的JWT。
- 域级角色:
第二步:替换过期依赖
停止使用官方已停更的keycloak-spring-boot-starter,该组件与Spring Boot 2.7+、Spring Security 5.7+版本兼容性极差,直接使用Spring Security原生的OAuth2 Client/Resource Server能力即可。第三步:自定义权限映射逻辑
你当前拿到的是OAuth2AuthenticationToken和DefaultOidcUser,属于OAuth2 Client(服务端跳转登录)场景,直接自定义OidcUserService实现角色提取即可,参考配置如下:
import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import java.util.*; import java.util.stream.Collectors; @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.oidcUserService(oidcUserService())) ); return http.build(); } private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { final OidcUserService delegate = new OidcUserService(); return userRequest -> { OidcUser oidcUser = delegate.loadUser(userRequest); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); // 提取域级角色 Map<String, Object> realmAccess = oidcUser.getClaim("realm_access"); if (realmAccess != null && realmAccess.containsKey("roles")) { List<String> realmRoles = (List<String>) realmAccess.get("roles"); mappedAuthorities.addAll(realmRoles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList())); } // 提取客户端级角色,将your-client-id替换为Keycloak中你的实际客户端ID Map<String, Object> resourceAccess = oidcUser.getClaim("resource_access"); if (resourceAccess != null && resourceAccess.containsKey("your-client-id")) { Map<String, Object> clientResource = (Map<String, Object>) resourceAccess.get("your-client-id"); if (clientResource != null && clientResource.containsKey("roles")) { List<String> clientRoles = (List<String>) clientResource.get("roles"); mappedAuthorities.addAll(clientRoles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList())); } } return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); }; } }
如果你是纯Resource Server场景(接口不做登录跳转,直接校验请求携带的JWT),直接自定义JWT权限转换器即可,参考配置如下:
@Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); // 权限前缀配置为ROLE_,适配hasRole()类的权限校验 converter.setAuthorityPrefix("ROLE_"); // 填写你之前确认的角色存储路径,域级角色填realm_access/roles,客户端角色填resource_access/your-client-id/roles converter.setAuthoritiesClaimName("realm_access/roles"); JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); jwtConverter.setJwtGrantedAuthoritiesConverter(converter); return jwtConverter; }
常见注意事项
- 角色前缀
ROLE_根据你的校验逻辑决定是否添加:如果用@PreAuthorize("hasRole('ADMIN')")做校验必须加前缀,如果用hasAuthority('ADMIN')则不需要加。 - Spring Security 5.4以下版本存在嵌套声明读取bug,如果配置后仍然提取不到角色,先升级Spring Security版本到5.4以上。
- 配置完成后重启服务,在接口中打断点查看
SecurityContextHolder.getContext().getAuthentication().getAuthorities()即可看到所有映射完成的角色。
内容的提问来源于stack exchange,提问作者Woodsman
相关产品推荐
相关产品推荐

