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 的角色参与授权。
备注:内容仅供参考。
