Spring Boot OAuth2 Client自定义TTL令牌缓存实现方案问询
自定义TTL或令牌返回值缓存OAuth2令牌解决方案
问题背景
在高负载Spring Boot应用中,使用spring-boot-starter-oauth2-client对接两个不同的OAuth2客户端(hello-client、hi-client),需要实现两种令牌缓存策略:
- 基于配置文件中自定义的TTL值缓存令牌
- 基于令牌提供商返回的
expires_in字段值缓存令牌
现有基础配置和客户端代码如下:
原有配置文件
spring: application: name: client-application security: oauth2: client: registration: hello-client: provider: hello-provider client-id: someusername client-secret: somepassword authorization-grant-type: client_credentials scope: resolve hi-client: provider: hi-provider client-id: anotherusername client-secret: anotherpassword authorization-grant-type: client_credentials scope: download provider: hello-provider: token-uri: https://tokenprovider.com/token hi-provider: token-uri: https://secureservice.com/token application: url: hello: https://somecoolcompany.com/api/v1.0/hello hi: https://veryawsomeendpoint.com/v2/hi
原有客户端代码
HelloServiceClient
@Service public class HelloServiceClient { private static final Logger log = LoggerFactory.getLogger(HelloServiceClient.class); private final RestClient restClient; public HelloServiceClient(RestClient.Builder builder, @Value("${application.url.hello}") String urlHello) { this.restClient = builder .baseUrl(urlHello) .requestInterceptor( (request, body, execution) -> { log.info("Lambda Interceptor: modifying before sending request"); ClientHttpResponse response = execution.execute(request, body); log.info("Lambda Interceptor: modifying after receiving response"); return response; } ) .build(); } public String hello() { return this.restClient.get() .uri("/hello") .attributes(clientRegistrationId("hello-client")) .retrieve() .body(String.class); } }
HiServiceClient
package vn.cloud.restclientdemo.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId; @Service public class HiServiceClient { private final RestClient restClient; public HiServiceClient(RestClient.Builder builder, @Value("${application.url.hi}") String urlHi) { this.restClient = builder .baseUrl(urlHi) .build(); } public String hi() { return this.restClient.get() .uri("/hi") .attributes(clientRegistrationId("hi-client")) .retrieve() .body(String.class); } }
解决方案一:基于自定义配置TTL缓存令牌
1. 扩展客户端配置属性
创建自定义配置类,支持读取每个客户端的ttl配置:
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.oauth2.client.registration.ClientRegistration; import java.time.Duration; import java.util.HashMap; import java.util.Map; @ConfigurationProperties(prefix = "spring.security.oauth2.client.registration") public class CustomClientRegistrationProps { private final Map<String, CustomClientRegistration> registration = new HashMap<>(); public Map<String, CustomClientRegistration> getRegistration() { return registration; } public static class CustomClientRegistration extends ClientRegistration { private Duration ttl; public Duration getTtl() { return ttl; } public void setTtl(Duration ttl) { this.ttl = ttl; } } }
在启动类添加注解启用配置属性:
@SpringBootApplication @EnableConfigurationProperties(CustomClientRegistrationProps.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2. 自定义令牌缓存管理器
实现OAuth2AuthorizedClientManager,按客户端ID区分缓存,并使用自定义TTL:
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import java.time.Instant; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class TtlBasedAuthorizedClientManager implements OAuth2AuthorizedClientManager { private final ClientRegistrationRepository clientRepo; private final OAuth2AuthorizedClientService clientService; private final CustomClientRegistrationProps customProps; private final ConcurrentHashMap<String, OAuth2AuthorizedClient> tokenCache = new ConcurrentHashMap<>(); public TtlBasedAuthorizedClientManager(ClientRegistrationRepository clientRepo, OAuth2AuthorizedClientService clientService, CustomClientRegistrationProps customProps) { this.clientRepo = clientRepo; this.clientService = clientService; this.customProps = customProps; } @Override public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest request) { String clientId = request.getClientRegistrationId(); // 检查缓存是否有效 OAuth2AuthorizedClient cachedClient = tokenCache.get(clientId); if (cachedClient != null && isCacheValid(cachedClient, clientId)) { return cachedClient; } // 获取新令牌并缓存 OAuth2AuthorizedClient newClient = clientService.loadAuthorizedClient(clientId, request.getPrincipal().getName()); tokenCache.put(clientId, newClient); // 定时清理过期缓存 Duration ttl = customProps.getRegistration().get(clientId).getTtl(); new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(ttl.toMillis()); tokenCache.remove(clientId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); return newClient; } private boolean isCacheValid(OAuth2AuthorizedClient client, String clientId) { Duration ttl = customProps.getRegistration().get(clientId).getTtl(); return client.getAccessToken().getIssuedAt().plus(ttl).isAfter(Instant.now()); } }
3. 注册自定义管理器并配置RestClient
@Bean public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRepo, OAuth2AuthorizedClientService clientService, CustomClientRegistrationProps customProps) { return new TtlBasedAuthorizedClientManager(clientRepo, clientService, customProps); } @Bean public RestClient.Builder restClientBuilder(OAuth2AuthorizedClientManager manager) { return RestClient.builder() .requestInterceptor(new OAuth2AuthorizedClientHttpRequestInterceptor(manager)); }
4. 更新配置文件添加TTL
spring: security: oauth2: client: registration: hello-client: # 原有配置... ttl: 9m hi-client: # 原有配置... ttl: 1m
解决方案二:使用令牌返回的expires_in值缓存令牌
1. 确保令牌响应正确解析expires_in
Spring Security默认会解析expires_in字段到OAuth2AccessToken的expiresAt属性,若需自定义解析逻辑,可实现令牌响应转换器:
import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseConverter; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponseConverter; import java.time.Duration; import java.time.Instant; import java.util.Map; public class ExpiresInTokenResponseConverter implements OAuth2AccessTokenResponseConverter<Map<String, Object>> { private final DefaultOAuth2AccessTokenResponseConverter delegate = new DefaultOAuth2AccessTokenResponseConverter(); @Override public OAuth2AccessTokenResponse convert(Map<String, Object> params) { OAuth2AccessTokenResponse response = delegate.convert(params); Integer expiresIn = (Integer) params.get("expires_in"); if (expiresIn != null) { Instant issuedAt = response.getAccessToken().getIssuedAt(); Instant expiresAt = issuedAt.plus(Duration.ofSeconds(expiresIn)); // 重新构建AccessToken,确保过期时间准确 OAuth2AccessToken newToken = new OAuth2AccessToken( response.getAccessToken().getTokenType(), response.getAccessToken().getTokenValue(), issuedAt, expiresAt ); return OAuth2AccessTokenResponse.withToken(newToken.getTokenValue()) .tokenType(newToken.getTokenType()) .expiresIn(expiresIn) .scopes(response.getAccessToken().getScopes()) .additionalParameters(response.getAdditionalParameters()) .build(); } return response; } }
2. 配置客户端使用自定义转换器
@Bean public ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() { ClientCredentialsOAuth2AuthorizedClientProvider provider = new ClientCredentialsOAuth2AuthorizedClientProvider(); DefaultClientCredentialsTokenResponseClient tokenClient = new DefaultClientCredentialsTokenResponseClient(); tokenClient.setAccessTokenResponseConverter(new ExpiresInTokenResponseConverter()); provider.setTokenResponseClient(tokenClient); return provider; } @Bean public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRepo, OAuth2AuthorizedClientService clientService) { DefaultOAuth2AuthorizedClientManager manager = new DefaultOAuth2AuthorizedClientManager(clientRepo, clientService); manager.setAuthorizedClientProvider(clientCredentialsProvider()); // 开启自动刷新过期令牌 manager.setContextAttributesMapper(context -> { OAuth2AuthorizeRequest authorizeRequest = (OAuth2AuthorizeRequest) context.get("authorizeRequest"); return Map.of(OAuth2AuthorizationContext.CLIENT_REGISTRATION_ID_ATTRIBUTE_NAME, authorizeRequest.getClientRegistrationId()); }); return manager; }
3. 基于令牌过期时间实现缓存
Spring Security的DefaultOAuth2AuthorizedClientManager会自动根据令牌的expiresAt判断是否需要刷新,无需额外自定义缓存逻辑;若需更精细控制,可参考解决方案一的缓存思路,将TTL替换为令牌的expiresIn值。
内容的提问来源于stack exchange,提问作者PatPanda
相关产品推荐
相关产品推荐

