Webflux+Spring Security下OAuth2客户端为何频繁触发身份提供商登录?
我们正在监控身份提供商的客户端登录情况,想明确Spring Security在此场景下的预期表现。
配置信息
@Configuration(proxyBeanMethods = false) class SecurityConfiguration { @Bean fun authorizedClientManager( clientRegistrationRepository: ReactiveClientRegistrationRepository, authorizedClientService: ReactiveOAuth2AuthorizedClientService, ): ReactiveOAuth2AuthorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService, ) .apply { val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build() setAuthorizedClientProvider(authorizedClientProvider) } @Bean fun customizer( authorizedClientManager: ReactiveOAuth2AuthorizedClientManager, ): WebClientCustomizer = WebClientCustomizer { it.filter(ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)) } }
业务逻辑
每15分钟执行一次以下逻辑:
client.doInitialRequest() .flatMap { client.doEvenMoreRequests() } // 约10k次请求
当前token有效期为5分钟(实际请求1分钟即可完成),采用client_credentials授权流。目前代码会导致身份提供商产生200至700次客户端登录。按预期,客户端完成初始请求后应复用已有的授权token,但实际并非如此。我们尝试将token有效期延长至1小时,发现首次请求周期后恢复正常,但1小时后又会出现200至700次登录。
问题原因分析
竞态条件导致重复获取token
默认的AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager没有针对token获取/刷新的并发控制。当token过期后,大量并发请求会同时检测到token失效,进而各自发起token获取请求,导致身份提供商收到大量登录请求。- 当token有效期为5分钟时,每15分钟的请求批次启动时,token已过期,所有请求都会触发新token获取,直接引发大量登录。
- 当token有效期为1小时时,前几个15分钟的请求批次还在token有效期内,所以能正常复用;但1小时后token过期,下一批次的并发请求又会同时触发token获取,再次出现大量登录。
多实例部署的额外影响(若存在)
如果你的服务是多实例部署,默认的InMemoryReactiveOAuth2AuthorizedClientService会让每个实例维护独立的token缓存。当token过期时,所有实例都会独立发起token请求,进一步放大登录次数。
解决方案
1. 添加并发控制,避免同一客户端重复发起token请求
通过包装ReactiveOAuth2AuthorizedClientManager,针对客户端ID添加锁机制,确保同一时间只有一个请求去获取/刷新token:
@Bean fun authorizedClientManager( clientRegistrationRepository: ReactiveClientRegistrationRepository, authorizedClientService: ReactiveOAuth2AuthorizedClientService, ): ReactiveOAuth2AuthorizedClientManager { val delegate = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService, ).apply { val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build() setAuthorizedClientProvider(authorizedClientProvider) } // 基于客户端ID的本地锁,单实例场景适用 val clientLocks = ConcurrentHashMap<String, Mono<Void>>() return ReactiveOAuth2AuthorizedClientManager { context -> val clientId = context.clientRegistrationId ?: return@ReactiveOAuth2AuthorizedClientManager Mono.empty() clientLocks.compute(clientId) { _, existingLock -> existingLock ?: delegate.authorize(context) .doFinally { clientLocks.remove(clientId) } .then(Mono.empty()) }?.then(delegate.authorize(context)) } }
2. 多实例场景:使用分布式token缓存
如果是多实例部署,替换默认的内存实现为分布式的ReactiveOAuth2AuthorizedClientService(比如基于Redis),让所有实例共享token缓存,避免每个实例单独获取token。
3. 调整token有效期与请求周期匹配
将token有效期设置为覆盖多个请求周期(比如30分钟以上),确保每次请求批次启动时token仍在有效期内,从根源减少token刷新的触发次数。
内容的提问来源于stack exchange,提问作者kschlesselmann

