Keycloak Realm角色映射Spring Authorities失效问题排查
问题分析与解决方案
核心原因:混淆了OAuth2客户端与资源服务器的权限处理逻辑
你同时启用了oauth2Login()(OIDC授权码登录流程)和oauth2ResourceServer()(Bearer Token资源验证流程),但两者的权限映射逻辑完全独立:
JwtAuthenticationConverter仅作用于资源服务器模式:当请求携带Authorization: Bearer <token>头访问受保护资源时,Spring才会用它解析JWT中的权限。- 而你当前走的是OIDC授权码登录流程,认证后用户权限由
OidcAuthorizationCodeAuthenticationProvider通过OidcUserService处理,和JwtAuthenticationConverter完全无关,这就是你的转换器没被调用的根本原因。
另外,Keycloak的Realm角色默认存储在ID Token的realm_access.roles字段中,而非顶级roles字段,这也是你直接调用setAuthoritiesClaimName("roles")不生效的额外原因。
解决方案:针对OIDC授权码流程自定义权限映射
1. 自定义OidcUserService提取Keycloak Realm角色
创建自定义的OidcUserService,从ID Token的realm_access.roles字段中提取角色并转为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.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser class CustomOidcUserService : OidcUserService() { override fun loadUser(userRequest: OidcUserRequest): OidcUser { // 先调用默认逻辑获取基础用户信息 val oidcUser = super.loadUser(userRequest) // 从ID Token的claims中提取realm_access.roles val realmAccess = oidcUser.claims["realm_access"] as? Map<String, Any> val roles = realmAccess?.get("roles") as? List<String> ?: emptyList() // 将角色转为GrantedAuthority,添加ROLE_前缀符合Spring Security规范 val authorities = roles.map { SimpleGrantedAuthority("ROLE_$it") } // 保留原有的权限(如scope) .plus(oidcUser.authorities) // 返回自定义权限的OidcUser return DefaultOidcUser(authorities, oidcUser.idToken, oidcUser.userInfo) } }
2. 将自定义OidcUserService配置到oauth2Login流程中
修改过滤器链配置,把自定义服务绑定到oauth2Login():
http.oauth2Login() .userInfoEndpoint() .oidcUserService(CustomOidcUserService()) // 绑定自定义用户服务 .and() // 你的其他oauth2Login配置... .and() .oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()) // 资源服务器模式的转换器仍保留(若需支持Bearer Token访问)
3. (可选)修复资源服务器模式的JWT转换器
如果同时需要支持Bearer Token访问API,需修改jwtAuthenticationConverter()来解析Keycloak的realm_access.roles:
import org.springframework.core.convert.converter.Converter import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter fun jwtAuthenticationConverter(): Converter<Jwt, AbstractAuthenticationToken> { val defaultConverter = JwtGrantedAuthoritiesConverter() val realmRoleConverter = Converter<Jwt, Collection<SimpleGrantedAuthority>> { jwt -> val realmAccess = jwt.claims["realm_access"] as? Map<String, Any> val roles = realmAccess?.get("roles") as? List<String> ?: emptyList() roles.map { SimpleGrantedAuthority("ROLE_$it") } } return Converter { jwt -> val authorities = mutableSetOf<SimpleGrantedAuthority>() // 添加默认的scope权限 authorities.addAll(defaultConverter.convert(jwt) ?: emptyList()) // 添加自定义的Realm角色权限 authorities.addAll(realmRoleConverter.convert(jwt) ?: emptyList()) JwtAuthenticationToken(jwt, authorities) } }
为什么教程中的方法无效?
多数教程会单独讲解OAuth2客户端登录或资源服务器验证的权限配置,很少覆盖两者同时启用的场景。你之前的配置只针对资源服务器模式,但实际走的是客户端登录流程,所以转换器不会被触发。
内容的提问来源于stack exchange,提问作者Johannes
相关产品推荐
相关产品推荐

