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

Spring Boot集成Keycloak遇403权限问题,求助角色验证配置

问题:Spring Boot集成Keycloak时角色权限校验返回403 Forbidden

问题背景

我正在尝试将Spring Boot应用与Keycloak集成,调用受角色保护的端点时,即使使用有效访问令牌,仍返回403 Forbidden错误。

解码后的JWT令牌

Keycloak颁发的JWT解码后内容如下(已创建clientApp1客户端,clientApp1User Realm角色并映射到用户):

{
 "alg": "RS256",
 "typ": "JWT",
 "kid": "ZWDbgcSI8nD2Yq4LA6hxYcsTbnf6y6Zj8PKyUobE_qE"
}.
{
 "exp": 1666444432,
 "iat": 1666444132,
 "jti": "e6883855-ef20-4fac-95dd-8f13bd0ae552",
 "iss": "http://localhost:12500/auth/realms/sampleRealm",
 "aud": "account",
 "sub": "80e1e45f-49fb-4a5a-9a60-b0057d291c53",
 "typ": "Bearer",
 "azp": "clientApp1",
 "session_state": "c22af762-7be9-4150-94d5-8bd35065ac57",
 "acr": "1",
 "allowed-origins": [
  "http://localhost:11501"
 ],
 "realm_access": {
  "roles": [
   "clientApp1User",
   "offline_access",
   "uma_authorization",
   "default-roles-samplerealm"
  ]
 },
 "resource_access": {
  "account": {
   "roles": [
    "manage-account",
    "manage-account-links",
    "view-profile"
   ]
  }
 },
 "scope": "email profile",
 "sid": "c22af762-7be9-4150-94d5-8bd35065ac57",
 "email_verified": false,
 "name": "user1FirstName User1LastName",
 "preferred_username": "user1",
 "given_name": "user1FirstName",
 "family_name": "User1LastName"
}.
[signature]

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>ResourceServerSample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ResourceServerSample</name>
    <description>ResourceServerSample</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

安全配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true)
public class SecurityConfig  {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors()
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().authorizeRequests()
            .anyRequest().permitAll()
            .and().oauth2ResourceServer().jwt();

        http.csrf().disable();

        return http.build();
    }
}

控制器代码

@RestController
public class TestControllers {

    // 公开端点
    @GetMapping("/welcome")
    public ResponseEntity<String> welcome() {
        return ResponseEntity.status(HttpStatus.OK).body("Welcome to the unprotected endpoint");
    }

//    @RolesAllowed("clientApp1User")
//    @Secured("clientApp1User")
    @PreAuthorize("hasAuthority('clientApp1User')")
    @GetMapping("/clientApp1User")
    public ResponseEntity<String> clientApp1User() {
        return ResponseEntity.status(HttpStatus.OK).body("clientApp1User protected endpoint sends its regards");
    }

    @PreAuthorize("hasAuthority('SCOPE_email')")
    @GetMapping("/testScope")
    public ResponseEntity<String> testScope() {
        return ResponseEntity.status(HttpStatus.OK).body("testScope protected endpoint sends its regards");
    }
}

问题现象

  • 使用@RolesAllowed("clientApp1User")、@Secured("clientApp1User")或@PreAuthorize("hasAuthority('clientApp1User')")保护的端点,调用时返回403 Forbidden;
  • 使用@PreAuthorize("hasAuthority('SCOPE_email')")或@PreAuthorize("hasAuthority('SCOPE_profile')")的端点,调用返回200 OK。

推测Spring Boot仅将scope声明中带SCOPE_前缀的值识别为权限,无法正确解析realm_access和resource_access中的角色。


解决方案

1. 自定义JWT权限转换器

实现JwtAuthenticationConverter,将JWT中的realm_access.roles和resource_access里的角色转换为Spring Security可识别的权限:

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

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        
        // 提取realm_access中的角色
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess != null) {
            List<String> realmRoles = (List<String>) realmAccess.get("roles");
            realmRoles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
        }

        // 提取resource_access中当前客户端的角色(以clientApp1为例)
        Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
        if (resourceAccess != null) {
            Map<String, Object> clientRolesMap = (Map<String, Object>) resourceAccess.get("clientApp1");
            if (clientRolesMap != null) {
                List<String> clientRoles = (List<String>) clientRolesMap.get("roles");
                clientRoles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
            }
        }

        // 保留原有的SCOPE权限
        authorities.addAll(baseConverter.convert(jwt));
        return authorities;
    });
    return converter;
}

2. 在安全配置中关联转换器

修改filterChain方法,将自定义转换器配置到oauth2资源服务器中:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors()
        .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().authorizeRequests()
        .anyRequest().permitAll()
        .and().oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter()); // 关联自定义转换器

    http.csrf().disable();

    return http.build();
}

3. 调整注解权限名称

由于转换器中给角色添加了ROLE_前缀,注解需要对应修改:

  • @RolesAllowed("ROLE_clientApp1User")
  • @Secured("ROLE_clientApp1User")
  • @PreAuthorize("hasAuthority('ROLE_clientApp1User')")

如果不需要前缀,可移除转换器中的"ROLE_"拼接,保持注解中的角色名称与JWT一致即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 23:55:25