You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot OAuth2 Client自定义TTL令牌缓存实现方案问询

自定义TTL或令牌返回值缓存OAuth2令牌解决方案

问题背景

在高负载Spring Boot应用中,使用spring-boot-starter-oauth2-client对接两个不同的OAuth2客户端(hello-client、hi-client),需要实现两种令牌缓存策略:

  1. 基于配置文件中自定义的TTL值缓存令牌
  2. 基于令牌提供商返回的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 00:14:54