如何正确地将嵌套的OpenID Connect(OIDC)声明映射到Spring Security 6的权限?
我正在将一个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();
}
- 我尝试创建一个自定义的
GrantedAuthoritiesMapper,但似乎JwtAuthenticationConverter已经在进入该逻辑之前就覆盖了它。 - 我试着在
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,它实现了类似的方法,但使用了 Optional 和 Streams。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。