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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:12:16