如何正确地将嵌套的OpenID Connect(OIDC)声明映射到Spring Security 6的权限?

后端开发 2026-07-11

我正在将一个Spring Boot 3.2应用从自定义认证服务器迁移到 Keycloak。在我之前的设置中,JWT中的角色是一个扁平列表。然而,Keycloak会将角色嵌套在一个 realm_access 对象中

{
  "sub": "12345",
  "realm_access": {
    "roles": ["admin", "user"]
  }
}

我正在使用 oauth2ResourceServer DSL。默认情况下,Spring Security会在 scope 声明中查找一个 SCOPE_ 前缀,但它似乎没有“看到”我的嵌套的Keycloak角色。

我的当前安全配置如下

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/admin/**").hasRole("admin") // This always returns 403
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

    return http.build();
}
  1. 我尝试创建一个自定义的 GrantedAuthoritiesMapper,但似乎 JwtAuthenticationConverter 已经在进入该逻辑之前就覆盖了它。
  2. 我试着在 application.yml 中设置 spring.security.oauth2.resourceserver.jwt.authorities-claim-name=realm_access.roles,但Spring似乎不原生支持对嵌套声明使用点记法。

在“Spring Security 6”的地道做法是什么,用来提取这些嵌套声明,并将它们转换为以 ROLE_ 为前缀的权限,以便 .hasRole("admin") 正确工作?我应该完全替换 JwtAuthenticationConverter 吗,还是有更简洁的方式提供自定义的声明提取器?

解决方案

我在某个项目里按如下方式配置SecurityFilterChain:

    @Bean
    public SecurityFilterChain securityFilterChain(
            final HttpSecurity http,
            final KeycloakAuthenticationConverter keycloakAuthenticationConverter) throws Exception {
        // ...
        http.oauth2ResourceServer(
                oauth -> oauth.jwt(
                        jwt -> jwt.jwtAuthenticationConverter(keycloakAuthenticationConverter)));

        // ...

        return http.build();
    }

    @Component
    public static final class KeycloakAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

        @Override
        public AbstractAuthenticationToken convert(@NonNull final Jwt jwt) {
            final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
            jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(this::convertGrantedAuthorities);
            return jwtAuthenticationConverter.convert(jwt);
        }

        private static List<GrantedAuthority> convertGrantedAuthorities(final Jwt jwt) {
            final JwtGrantedAuthoritiesConverter scopeConverter = new JwtGrantedAuthoritiesConverter();

            final Collection<GrantedAuthority> roles = new ArrayList<>(scopeConverter.convert(jwt));
            roles.add(extractKeycloakRoles(jwt));
            return roles;
        }

        private static Collection<GrantedAuthority> extractKeycloakRoles(final Jwt jwt) {
            final Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
            if (realmAccess == null) {
                return Collections.emptyList();
            }
            final Collection<String> roles = realmAccess.get("roles");
            if (roles == null) {
                return Collections.emptyList();
            }
            return roles.stream()
                    .map(role -> "ROLE_" + role)
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
    }

另请参阅 https://www.baeldung.com/spring-boot-keycloak,它实现了类似的方法,但使用了 OptionalStreams。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章