PAYATHON 2026

Spring Security 6 如何将 Keycloak 嵌套角色映射为权限?

数据小陈

可以保留 JwtAuthenticationConverter,通过 setJwtGrantedAuthoritiesConverter(...) 提供一个 Converter<Jwt, Collection<GrantedAuthority>> 来提取 realm_access.roles,再将配置好的 converter 显式接入 oauth2ResourceServer().jwt(...)。这个扩展点在 Spring Security 6.2 中已经提供,适用于题述的 Spring Boot 3.2 场景。API 文档

为什么默认配置匹配不到角色

默认的 JwtGrantedAuthoritiesConverter 从顶层 scope 或 scp claim 中提取权限,并为提取出的值添加 SCOPE_ 前缀。它不会自动遍历 realm_access.roles。

SCOPE_ 前缀是在权限转换阶段添加的,JWT 中的值无需预先带有这个前缀。

setAuthoritiesClaimName("realm_access.roles") 会按字面指定 claim 名称,不会把点号解释成嵌套路径。只修改权限前缀也无法读取嵌套字段。Spring Security 6.2 源码

配置自定义角色提取逻辑

以下配置保留默认的 scope 权限,并合并 Keycloak realm roles。示例假设应用已经通过 issuer-uri 或自定义 JwtDecoder 配置好了 JWT 验证。

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter scopeConverter =
                new JwtGrantedAuthoritiesConverter();

        JwtAuthenticationConverter converter =
                new JwtAuthenticationConverter();

        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Collection<GrantedAuthority> authorities =
                    new LinkedHashSet<>(scopeConverter.convert(jwt));

            Object realmAccessClaim = jwt.getClaims().get("realm_access");
            if (!(realmAccessClaim instanceof Map<?, ?> realmAccess)) {
                return authorities;
            }

            Object rolesClaim = realmAccess.get("roles");
            if (!(rolesClaim instanceof Collection<?> roles)) {
                return authorities;
            }

            for (Object value : roles) {
                if (value instanceof String role && !role.isBlank()) {
                    authorities.add(
                            new SimpleGrantedAuthority("ROLE_" + role));
                }
            }

            return authorities;
        });

        return converter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http,
            JwtAuthenticationConverter jwtAuthenticationConverter)
            throws Exception {

        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/admin/**").hasRole("admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter)
                )
            );

        return http.build();
    }
}

setJwtGrantedAuthoritiesConverter(...) 会替换权限提取策略,所以示例主动调用默认的 scopeConverter,避免丢失已有的 SCOPE_ 权限。LinkedHashSet 用于合并和去重。

如果 realm_access 或 roles 缺失,示例只返回 scope 权限。非字符串和空白角色会被忽略,不会因此授予角色权限。

hasRole 应该如何匹配

在默认角色前缀配置下,.hasRole("admin") 检查的是 ROLE_admin。对于题目给出的 JWT,上面的代码会生成:

ROLE_admin
ROLE_user

可以保留原有规则:

.requestMatchers("/api/admin/**").hasRole("admin")

也可以显式检查完整权限名称:

.requestMatchers("/api/admin/**").hasAuthority("ROLE_admin")

角色名称区分大小写,ROLE_admin 与 ROLE_ADMIN 不相同。示例假设 JWT 中保存的是 admin、user 这样的原始角色名;如果 claim 已经带有 ROLE_,需要调整拼接逻辑,避免重复添加前缀。

为什么 GrantedAuthoritiesMapper 没有生效

在题述的 JWT Resource Server 流程中,权限转换的直接入口是 JwtAuthenticationConverter。单独声明一个 GrantedAuthoritiesMapper Bean,不会让它自动参与这条认证链路。

因此,不能仅凭这个现象判断它的结果被提前覆盖,应先确认它是否实际接入了认证流程。直接配置 JWT 的权限 converter 就能完成这里所需的映射。Resource Server 认证流程与配置说明

修改后仍返回 403 时检查什么

先确认请求携带的 access token 中确实存在 realm_access.roles,并包含字符串 admin。不要只检查 ID Token;用户在 Keycloak 后台拥有角色,也不代表当前 access token 一定包含该角色。

接着检查当前请求的 Authentication#getAuthorities() 是否包含 ROLE_admin,并确认请求命中了配置该 converter 的 SecurityFilterChain。如果权限已正确生成,再检查其他授权规则、方法级安全,以及相关请求是否触发了 CSRF 校验。

这段代码只映射 realm_access.roles。如果实际使用的是 client roles,需要针对实际的 resource_access 结构另写提取逻辑,并明确允许哪个 client 的角色参与授权。

备注:内容仅供参考。