Spring Security集成Keycloak:Bearer Token请求返回403错误排查
嘿,我之前在Spring Security + Keycloak的Kotlin项目里也碰到过一模一样的403问题,大概率是某个配置环节没衔接上,给你列几个最容易踩坑的点,你逐一排查下:
1. 没正确映射Keycloak令牌里的角色到Spring Security权限
Keycloak的角色默认存在realm_access.roles或者客户端专属的resource_access[your-client-id].roles里,但Spring Security不会自动识别这些字段。你得配置一个JWT转换器来做映射:
@Bean fun jwtAuthenticationConverter(): JwtAuthenticationConverter { val authoritiesConverter = JwtGrantedAuthoritiesConverter() // 从realm的角色列表里提取权限,前缀加ROLE_适配Spring的默认规则 authoritiesConverter.setAuthoritiesClaimName("realm_access.roles") authoritiesConverter.setAuthorityPrefix("ROLE_") val converter = JwtAuthenticationConverter() converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter) return converter }
然后一定要在你的SecurityFilterChain里把这个转换器用上:
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http .authorizeHttpRequests { auth -> auth.anyRequest().authenticated() } .oauth2ResourceServer { oauth2 -> oauth2.jwt { jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()) } } return http.build() }
2. 配置文件里的Issuer URI和Keycloak不匹配
检查你的application.yml(或properties)里的 issuer 地址,必须和Keycloak Realm的Issuer URI完全一致(包括端口、realm名称):
spring: security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:8080/realms/your-realm-name
另外要确认Keycloak客户端的Access Type设置正确(前后端分离用public,服务间调用用confidential),并且Web Origins/Redirect URIs配置符合你的请求来源。
3. 没开启方法级安全(如果用了@PreAuthorize之类的注解)
如果你的控制器用了@PreAuthorize("hasRole('ADMIN')")这类注解,必须在配置类上加上@EnableMethodSecurity来启用:
@Configuration @EnableMethodSecurity(prePostEnabled = true) class SecurityConfig { // 你的配置代码 }
还要注意Keycloak的角色是大小写敏感的,比如ADMIN和admin会被Spring当成两个不同的权限。
4. 令牌本身有问题
用jwt.io解析你的Bearer令牌,检查这几个点:
exp字段:令牌有没有过期iss字段:是不是和配置里的Issuer URI完全一致- 角色字段:有没有包含你需要的权限(比如
realm_access.roles里有没有对应的角色)
也可以开启Spring Security的调试日志,看看认证过程到底哪里失败了:
logging: level: org.springframework.security: DEBUG
5. 跨域请求的CORS配置没放行Authorization头
如果你的请求是跨域的,Spring Security默认会拦截带自定义头的请求,得配置CORS允许携带Authorization:
@Bean fun corsConfigurationSource(): CorsConfigurationSource { val config = CorsConfiguration() config.allowedOrigins = listOf("http://your-frontend-domain") // 替换成你的前端地址 config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE") config.allowedHeaders = listOf("*") config.exposedHeaders = listOf("Authorization") config.allowCredentials = true val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", config) return source }
然后在filterChain里加上.cors { it.configurationSource(corsConfigurationSource()) }。
内容的提问来源于stack exchange,提问作者Maurice Müller

