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

Spring Boot 3集成Keycloak权限配置问题求助

Spring Boot 3 + Spring Security 集成Keycloak问题排查与自定义Token转换器方案

问题描述

在Spring Boot 3与Spring Security新版本中配置Keycloak时遇到两个核心问题:

  • 编写SecurityFilterChain后,系统跳过认证校验逻辑,只要持有有效Bearer Token的用户就能访问受保护的/api/**资源
  • Spring Security默认只识别JWT中的scope作为权限,未将Keycloak颁发的角色转换为Granted Authorities

另外,给JwtAuthConverterProperties添加@Configuration注解后,所有请求被拒绝,询问自定义Token转换器是否可行。


相关代码

WebSecurityConfig.java

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {

    private final JwtAuthConverter jwtAuthConverter;

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception  {
        http.oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(jwtAuthConverter);
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**","api/**","/api").hasAuthority(ADMINISTRATION)
                .anyRequest().authenticated()
                );
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.cors().and().csrf().disable();
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties oAuth2ResourceServerProperties) {
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(oAuth2ResourceServerProperties.getJwt().getJwkSetUri()).build();
        jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(oAuth2ResourceServerProperties.getJwt().getIssuerUri()));
        return jwtDecoder;
    }

    private static final String ADMINISTRATION = "ROLE_administration";
}

JwtAuthConverter.java

@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    private final JwtAuthConverterProperties properties;
    public JwtAuthConverter(JwtAuthConverterProperties properties) {
        this.properties = properties;
    }

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        Collection<GrantedAuthority> authorities = Stream.concat(
                jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
                extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
        return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
    }

    private String getPrincipalClaimName(Jwt jwt) {
        String claimName = JwtClaimNames.SUB;
        if (properties.getPrincipalAttribute() != null) {
            claimName = properties.getPrincipalAttribute();
        }
        return jwt.getClaim(claimName);
    }

    private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
        Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
        Map<String, Object> resource;
        Collection<String> resourceRoles;
        if (resourceAccess == null
                || (resource = (Map<String, Object>) resourceAccess.get(properties.getResourceId())) == null
                || (resourceRoles = (Collection<String>) resource.get("roles")) == null) {
            return Collections.emptySet();
        }
        return resourceRoles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toSet());
    }
}

JwtAuthConverterProperties.java

@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "jwt.auth.converter")
public class JwtAuthConverterProperties {

    @NotBlank
    private String resourceId;
    private String principalAttribute;
}

问题分析与解决方案

1. 自定义Token转换器完全可行

你的JwtAuthConverter实现思路正确,核心逻辑是从JWT的resource_access字段中提取Keycloak角色并转换为Granted Authority,同时保留默认的scope权限。问题出在配置细节上。

2. 跳过认证链的原因与修正

  • 路径规则错误:requestMatchers中的"api/**"缺少前缀斜杠,导致该规则不生效,只有"/api/**"和"/api"被匹配。无效规则会导致权限校验被绕过,只要Token有效就能访问所有/api开头的资源。
  • 修正方案:统一路径规则为"/api/**",覆盖所有子路径。

3. 添加@Configuration后请求被拒绝的原因与修正

JwtAuthConverterProperties同时标注@Configuration和@ConfigurationProperties,会被Spring视为配置类,但该类依赖外部配置的resourceId,如果配置文件中未正确设置jwt.auth.converter.resource-id,@NotBlank校验会失败,导致上下文初始化异常,最终所有请求被拒绝。

  • 修正方案:
    1. 移除JwtAuthConverterProperties上的@Configuration注解
    2. 在WebSecurityConfig中添加@EnableConfigurationProperties(JwtAuthConverterProperties.class)启用配置绑定
    3. 在配置文件(如application.yml)中添加Keycloak客户端ID配置:
      jwt:
        auth:
          converter:
            resource-id: 你的Keycloak客户端ID
      

4. 角色未纳入Granted Authorities的验证点

  • 确认JwtAuthConverter上的@Component注解生效,Spring能扫描到该类
  • 检查JWT的resource_access字段是否包含对应客户端的角色列表,示例格式:
    "resource_access": {
      "你的客户端ID": {
        "roles": ["administration"]
      }
    }
    
    转换器会将其转换为ROLE_administration,与你定义的ADMINISTRATION常量匹配。

修正后的核心配置

调整后的WebSecurityConfig.java

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
@EnableConfigurationProperties(JwtAuthConverterProperties.class) // 启用配置绑定
public class WebSecurityConfig {

    private final JwtAuthConverter jwtAuthConverter;

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception  {
        http.oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(jwtAuthConverter);
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").hasAuthority(ADMINISTRATION) // 统一路径规则
                .anyRequest().authenticated()
                );
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.cors().and().csrf().disable();
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties oAuth2ResourceServerProperties) {
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(oAuth2ResourceServerProperties.getJwt().getJwkSetUri()).build();
        jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(oAuth2ResourceServerProperties.getJwt().getIssuerUri()));
        return jwtDecoder;
    }

    private static final String ADMINISTRATION = "ROLE_administration";
}

调整后的JwtAuthConverterProperties.java

@Data
@Validated
@ConfigurationProperties(prefix = "jwt.auth.converter")
public class JwtAuthConverterProperties {

    @NotBlank
    private String resourceId;
    private String principalAttribute;
}

内容的提问来源于stack exchange,提问作者Ivan Tomić

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 12:40:14