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
相关产品推荐
相关产品推荐

