KeyCloak bearer-only客户端凭证错误仍可调用安全端点问题排查
这个问题其实是Spring Boot资源服务器和KeyCloak集成时的常见配置疏漏——核心是你的服务只验证了令牌本身的合法性(签名、过期时间),却没限制令牌的来源或受众,导致任何来自KeyCloak的有效令牌都能访问你的安全端点。下面具体分析原因和修复步骤:
核心原因:资源服务器未校验令牌的客户端/受众信息
当你用合法客户端cli1拿到令牌后,KeyCloak已经完成了对client-id和client-secret的验证——这一步是在令牌颁发阶段完成的,而你的Spring Boot服务作为资源服务器,默认职责只是确认令牌是由信任的KeyCloak签发、未过期的,但不会主动校验这个令牌是不是专门给你的服务(或指定客户端)颁发的。
具体来说,你可能遇到了以下几种配置问题:
1. 未配置aud(受众)验证
KeyCloak签发的JWT令牌里会包含aud字段,代表这个令牌的目标受众(比如某个客户端ID或资源ID)。如果你的服务没配置验证aud,那么任何来自该KeyCloak realm的有效令牌,不管它是给哪个客户端的,都会被服务端放行。
比如你的application.yml里可能缺少了关键的受众配置:
spring: security: oauth2: resourceserver: jwt: audience: your-service-client-id # 这里要填你的服务对应的KeyCloak客户端ID
2. 未通过权限(Scope)限制访问
就算令牌有效,如果你的服务没要求访问端点必须具备特定的scope,那么任何有效令牌都能访问。比如你可能没在安全配置里添加scope校验:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 要求访问/secure/**的令牌必须包含指定scope .requestMatchers("/secure/**").hasAuthority("SCOPE_your-required-scope") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); }
合法客户端cli1可能被KeyCloak配置了这个scope,但错误配置的客户端不会有,这样就能限制非法访问。
3. 资源服务器模式理解偏差
很多人会误以为资源服务器要重新验证client-id和client-secret,但其实这是客户端模式的逻辑——资源服务器的核心是验证令牌的合法性,而客户端身份已经在KeyCloak颁发令牌时验证过了。如果要限制只有特定客户端的令牌能访问,必须通过aud或scope来实现,而不是重复验证客户端凭证。
验证与修复步骤
- 解析令牌确认内容:用JWT解析工具(比如本地的JWT库或者离线解析工具)打开你的Bearer令牌,查看
aud(受众)、azp(授权方,即签发该令牌的客户端ID)字段,确认这些值是否符合你的预期。 - 添加受众验证配置:在Spring Boot配置里指定
audience,确保只有目标受众的令牌能被服务端接受。 - 配置权限限制:在安全配置中添加scope或角色校验,确保只有具备相应权限的令牌能访问安全端点。
- 检查KeyCloak客户端配置:确认你的服务对应的KeyCloak客户端
Access Type和权限配置正确,比如是否给合法客户端分配了所需的scope。
给你一个完整的配置示例参考:
application.yml
spring: security: oauth2: resourceserver: jwt: issuer-uri: https://your-keycloak-domain/auth/realms/your-realm audience: your-service-client-id
SecurityConfig.java
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/secure/**").hasAuthority("SCOPE_access-secure-api") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(customJwtConverter()) ) ); return http.build(); } private JwtAuthenticationConverter customJwtConverter() { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthorityPrefix("SCOPE_"); authoritiesConverter.setAuthoritiesClaimName("scope"); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; } }
这样配置后,只有包含正确aud和指定scope的令牌才能访问你的安全端点,其他不符合条件的令牌会被服务端拒绝。
内容的提问来源于stack exchange,提问作者codependent

