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

Spring Security集成Keycloak:JWT权限映射与aud校验问题

问题描述

我们的生产应用已上线,需要校验JWT的aud字段并支持自定义受众,但Keycloak自带的aud校验无法满足需求。参考Spring Security文档配置SecurityConfig后,已成功实现aud校验,但此时SecurityFilterChain不再使用org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount,转而使用org.springframework.security.oauth2.jwt.Jwt(因添加了http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);来定制aud校验器)。

当前系统报错,提示scope与SecurityFilterChain中的角色不匹配——我们的角色存储在JWT的独立声明realm_access.roles中,但尝试grantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access[roles]");等方式均无法成功从该声明中获取权限,请求解决权限映射问题。

JWT声明示例:

{sub=316232a4-2054-4b98-ab28-756cf767fa64, 
resource_access={"account":{"roles":["manage-account","manage-account-links","view-profile"]}},
clientHost=192.168.0.126, 
email_verified=false, 
clientId=test-client, 
iss=https://url.net/auth/realms/app, 
typ=Bearer, 
preferred_username=service-account-test-backoffice, 
clientAddress=192.168.0.126, aud=[kibana, account], 
acr=1, 
nbf=1970-01-01T00:00:00Z, 
realm_access={"roles":["backoffice","offline_access","uma_authorization"]}, 
azp=test-client, 
auth_time=0, 
scope=email profile, 
exp=2023-05-05T13:34:23Z, 
session_state=8b2877a3-9e04-44b2-8f52-43e079c446a9, 
iat=2023-05-05T13:14:23Z, 
jti=db589f9d-44d1-4bc0-813c-e34617b3ba64}

解决方案

Spring Security默认的JwtGrantedAuthoritiesConverter只会从scope或scp声明中提取权限,无法直接处理嵌套的realm_access.roles结构,需要自定义权限转换逻辑,以下是两种可行实现方式:

方式一:自定义JwtAuthenticationConverter

创建自定义转换器,同时支持提取realm_access.roles角色与默认scope权限:

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final JwtGrantedAuthoritiesConverter defaultConverter = new JwtGrantedAuthoritiesConverter();

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        // 获取默认的scope权限
        Collection<GrantedAuthority> authorities = defaultConverter.convert(jwt);
        
        // 从realm_access.roles中提取角色并添加ROLE_前缀(适配Spring Security的hasRole规则)
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess != null && realmAccess.containsKey("roles")) {
            List<String> roles = (List<String>) realmAccess.get("roles");
            authorities.addAll(roles.stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                    .collect(Collectors.toList()));
        }
        
        return new JwtAuthenticationToken(jwt, authorities);
    }
}

在SecurityConfig中配置使用该转换器:

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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.OAuth2TokenValidator;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(new CustomJwtAuthenticationConverter())
                    // 配置你已实现的aud校验器
                    .audienceValidator(audienceValidator())
                )
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("backoffice")
                .anyRequest().authenticated()
            );
        return http.build();
    }

    // 你的自定义aud校验器实现
    private OAuth2TokenValidator<Jwt> audienceValidator() {
        return new AudienceValidator();
    }
}

方式二:扩展JwtGrantedAuthoritiesConverter

若不需要保留默认scope权限,可直接扩展默认转换器,仅提取realm_access.roles:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class KeycloakRealmRolesConverter extends JwtGrantedAuthoritiesConverter {

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess == null || !realmAccess.containsKey("roles")) {
            return super.convert(jwt);
        }
        
        List<String> roles = (List<String>) realmAccess.get("roles");
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
    }
}

在配置中替换默认转换器:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    KeycloakRealmRolesConverter rolesConverter = new KeycloakRealmRolesConverter();
    JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
    jwtConverter.setJwtGrantedAuthoritiesConverter(rolesConverter);

    http
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                .jwtAuthenticationConverter(jwtConverter)
                .audienceValidator(audienceValidator())
            )
        )
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/admin/**").hasRole("backoffice")
            .anyRequest().authenticated()
        );
    return http.build();
}

关键注意点

  • 角色前缀:如果使用hasRole("角色名"),Spring Security会自动匹配带ROLE_前缀的权限,因此转换器中需要添加该前缀;若使用hasAuthority("角色名"),则无需添加前缀,直接使用原始角色名即可。
  • 权限合并:方式一保留了默认的scope权限提取,若不需要可删除相关代码,仅处理realm_access.roles。

内容的提问来源于stack exchange,提问作者Stieg

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 21:30:44