Spring OAuth 2.3.8与Spring Security 5.4.6版本冲突排查:外部OAuth2服务调用获取令牌失败问题
从你的代码和遇到的问题来看,核心矛盾在于Spring Security 5.x的默认匿名认证逻辑和旧版spring-security-oauth2的AccessTokenProvider行为不兼容——后者在获取外部OAuth2令牌时,预期SecurityContext里的Authentication是null,但Spring Security 5.x默认会自动填充AnonymousAuthenticationToken,这就直接导致AccessTokenProviderChain的逻辑判断出错,拿不到令牌。
下面给你整理几个实用的排查和解决思路:
1. 先确认版本依赖的冲突点
首先得搞清楚你用的Spring Security和spring-security-oauth2版本是不是匹配:
- 如果你的项目是Spring Boot 2.x及以上,默认带的Spring Security是5.x版本,而旧的
spring-security-oauth2(非Spring官方当前主推的OAuth2模块)是基于Spring Security 4.x开发的,两者在API和上下文处理上差异很大。 - 去查看你的
pom.xml或者build.gradle,检查是否同时引入了spring-security-web/spring-security-config和spring-security-oauth2,版本是否适配。
提个醒:Spring Boot 2.x之后官方推荐使用
spring-security-oauth2-client和spring-security-oauth2-resource-server模块,旧的spring-security-oauth2已停止维护。如果能迁移的话,优先考虑换用官方新模块,从根源上解决版本冲突问题。
2. 调整SecurityContext的匿名认证规则
如果暂时没法迁移到新模块,那可以通过修改Spring Security配置,让调用外部服务的线程里,SecurityContext不会被填充AnonymousAuthenticationToken:
方案A:调用外部服务时手动清空上下文
在调用外部API的代码里,先保存当前的SecurityContext,清空之后再调用,完事再恢复回去:
// 先把原上下文存起来 val originalContext = SecurityContextHolder.getContext() try { // 清空上下文,让OAuth2RestTemplate拿令牌时能拿到null SecurityContextHolder.clearContext() // 这里调用你的外部服务API externalRestTemplate.getForObject("/external/api", YourResponse::class.java) } finally { // 不管调用成功还是失败,都把原上下文恢复回去 SecurityContextHolder.setContext(originalContext) }
方案B:修改全局Security配置,禁用或限制匿名认证
在你的SecurityConfig里,调整匿名认证的配置,要么直接禁用,要么只针对特定路径生效:
override fun configure(http: HttpSecurity) { http.csrf().disable() .authorizeRequests() .antMatchers("/firstapi/**", "/secondapi/**").authenticated() .anyRequest().permitAll() .and() .httpBasic() // 这里可以直接禁用全局匿名认证,或者设置匹配器只针对公开路径启用 .anonymous() .disable() }
注意:禁用全局匿名认证之前,得先确认你的应用有没有不需要认证的公开接口,别影响了现有业务。
3. 替换成官方的OAuth2客户端实现
如果条件允许,真心建议你迁移到Spring Security官方的OAuth2客户端模块,彻底解决版本冲突:
- 先把旧的
spring-security-oauth2依赖删掉,引入官方的spring-security-oauth2-client:implementation 'org.springframework.security:spring-security-oauth2-client' - 在配置文件里添加OAuth2客户端的参数(比如application.yml):
spring: security: oauth2: client: registration: external-service: client-id: 你的客户端ID client-secret: 你的客户端密钥 authorization-grant-type: password username: 外部服务的用户名 password: 外部服务的密码 provider: external-service: token-uri: ${http.external.api.security.oauth2.token-uri} - 用官方的方式配置RestTemplate:
@Bean fun externalRestTemplate(authorizedClientManager: OAuth2AuthorizedClientManager): RestTemplate { val template = RestTemplate() // 添加OAuth2授权拦截器,自动处理令牌获取和刷新 template.interceptors.add(OAuth2AuthorizationRequestInterceptor(authorizedClientManager, OAuth2AuthorizeRequest.withClientRegistrationId("external-service").build())) // 这里可以继续加你原来的messageConverters、errorHandler等配置 return template } @Bean fun authorizedClientManager( clientRegistrationRepository: ClientRegistrationRepository, authorizedClientService: OAuth2AuthorizedClientService ): OAuth2AuthorizedClientManager { val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .password() // 用密码模式获取令牌 .build() val manager = DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService) manager.setAuthorizedClientProvider(authorizedClientProvider) return manager }
这种方式完全基于Spring Security 5.x的原生实现,不会再有版本冲突的问题。
4. 调试SecurityContext的填充过程
如果上面的方法都不管用,那可以加调试代码,看看AnonymousAuthenticationToken到底是在哪一步被填充进去的:
- 在
AccessTokenProviderChain.obtainAccessToken方法上打个断点,查看调用栈,找到填充匿名认证的代码位置。 - 检查有没有自定义的Filter或者Interceptor,在调用外部服务之前就把SecurityContext给填充了。
- 看看Spring Security的
AnonymousAuthenticationFilter什么时候执行,是不是在OAuth2RestTemplate的调用线程里被触发了。
内容的提问来源于stack exchange,提问作者darth jemico

