Spring Boot 3.1.3中OAuth2令牌缓存与刷新配置咨询
我在Spring Boot 3.1.3环境下,结合Spring Security与WebClient搭建服务,流程如下:
- 从队列拉取消息(非HTTP请求触发)
- 处理消息
- 向认证服务器请求OAuth2令牌
- 携带令牌通过WebClient将处理后的数据发送至OAuth2认证的第三方API
- 目标:在令牌有效期(1小时)内复用令牌,过期前自动刷新
目前已实现前4步,但每次调用第三方API都会重新请求令牌,未实现复用。需要调整配置实现令牌缓存与自动刷新。
另外,查看ServerOAuth2AuthorizedClientExchangeFilterFunction的JavaDoc时看到示例有.attributes(oauth2AuthorizedClient(authorizedClient)),想知道这是否和令牌缓存有关,以及如何获取authorizedClient。
现有配置
Configuration Class
package com.company.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class SomeConfig { private static final String REMOTE_SERVER_URL = "https://www.remote.com"; private static final String OAUTH_CLIENT_REGISTRATION = "example"; @Bean public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveOAuth2AuthorizedClientService authorizedClientService) { ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .authorizationCode() .refreshToken() .clientCredentials() .build(); AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } @Bean WebClient httpClient( ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Client.setDefaultClientRegistrationId(OAUTH_CLIENT_REGISTRATION); return WebClient.builder() .filter(oauth2Client) .baseUrl(REMOTE_SERVER_URL) .build(); } }
Request Service
package com.company.example; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Service public class RequestService { private static final String SERVER_ENDPOINT = "/endpoint"; private final WebClient webClient; public RequestService(WebClient webClient) { this.webClient = webClient; } public Mono<ResponseObject> makeRequest(Mono<RequestObject> requestObject) { return requestObject.flatMap(req -> this.webClient .post() .uri(SERVER_ENDPOINT) .header(ACCEPT, APPLICATION_JSON_VALUE) .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .bodyValue(requestObject) .retrieve() .bodyToMono(ResponseObject.class)); } }
application.yaml
spring: security: oauth2: client: registration: example: authorization-grant-type: client_credentials client-id: ${client-id} client-secret: ${client-secret} scope: api://${app-id}/.default provider: example: token-uri: https://example.oauth.server.com/oauth2/v2.0/token
Dependencies
<?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 http://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>3.1.3</version> <relativePath/> </parent> <groupId>com.example.oauth</groupId> <artifactId>webclient</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> </dependencies> </project>
解决方案
核心问题分析
当前使用的AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager默认会将授权客户端存储到ReactiveOAuth2AuthorizedClientService中,但非HTTP触发场景(比如队列消费)缺少OAuth2AuthorizedClient的关联上下文,导致每次请求都会重新获取令牌。另外,授权客户端提供者包含的authorizationCode()和refreshToken()模式对于客户端凭证模式是多余的,会增加不必要的逻辑。
调整配置步骤
1. 优化ReactiveOAuth2AuthorizedClientManager
针对客户端凭证模式简化授权提供者,并配置默认客户端注册ID解析器,适配非HTTP场景:
@Bean public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveOAuth2AuthorizedClientService authorizedClientService) { // 只保留clientCredentials模式,匹配当前授权类型 ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build(); AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); // 为非HTTP场景设置默认客户端注册ID,避免每次请求都需要指定 authorizedClientManager.setContextAttributesMapper(context -> Mono.just(Collections.singletonMap( OAuth2AuthorizedClientManagerUtils.DEFAULT_CLIENT_REGISTRATION_ID_ATTRIBUTE_NAME, OAUTH_CLIENT_REGISTRATION))); return authorizedClientManager; }
2. 修改RequestService实现令牌复用
注入ReactiveOAuth2AuthorizedClientManager,主动获取授权客户端并传递给WebClient,确保复用有效令牌:
package com.company.example; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.Collections; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.security.oauth2.client.OAuth2AuthorizedClientManagerUtils.DEFAULT_CLIENT_REGISTRATION_ID_ATTRIBUTE_NAME; @Service public class RequestService { private static final String SERVER_ENDPOINT = "/endpoint"; private final WebClient webClient; private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager; private static final String OAUTH_CLIENT_REGISTRATION = "example"; public RequestService(WebClient webClient, ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { this.webClient = webClient; this.authorizedClientManager = authorizedClientManager; } public Mono<ResponseObject> makeRequest(Mono<RequestObject> requestObject) { // 先获取授权客户端:自动复用有效令牌,过期则重新获取 return authorizedClientManager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId(OAUTH_CLIENT_REGISTRATION).build()) .flatMap(authorizedClient -> requestObject.flatMap(req -> this.webClient .post() .uri(SERVER_ENDPOINT) .header(ACCEPT, APPLICATION_JSON_VALUE) .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) // 显式传递已授权客户端,让WebClient直接使用其令牌 .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient)) .bodyValue(req) // 修正原代码错误,使用单个请求对象而非Mono .retrieve() .bodyToMono(ResponseObject.class))); } }
3. 关于.attributes(oauth2AuthorizedClient(authorizedClient))的说明
这个方法在非HTTP场景下至关重要:因为队列消费没有请求上下文,ServerOAuth2AuthorizedClientExchangeFilterFunction无法自动关联授权客户端,必须显式传递。
获取authorizedClient的方式是通过ReactiveOAuth2AuthorizedClientManager的authorize方法,传入指定客户端注册ID的OAuth2AuthorizeRequest,该方法会自动:
- 检查是否存在未过期的有效令牌
- 存在则直接返回已授权客户端
- 令牌过期/不存在则自动发起新的令牌请求(客户端凭证模式下无refresh token,重新获取即完成"刷新")
验证效果
修改后可通过日志观察认证服务器请求:
- 第一次调用触发令牌请求
- 1小时内后续调用复用现有令牌,无新请求
- 令牌过期后,下一次调用自动获取新令牌
补充说明
- 客户端凭证模式下,Spring Security不会返回refresh token,"刷新"逻辑实际是重新获取新令牌
- 默认
ReactiveOAuth2AuthorizedClientService使用内存存储,生产分布式部署可替换为Redis等分布式存储实现
内容的提问来源于stack exchange,提问作者matsev

