You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 01:50:26