Reactive Spring OAuth2用refresh token访问TD Ameritrade API配置问题
核心问题
你当前配置无法正常工作的原因有两点:
- Spring Security内置的
refresh_token授权逻辑设计上只负责续期已存在的令牌,不会主动发起首次令牌申请。只有当本地已经持久化了包含有效refresh_token的授权客户端记录,且对应access_token过期时,才会自动触发刷新流程。你没有为TD Ameritrade客户端预置初始refresh_token,框架找不到可用的刷新凭据,自然不会发起令牌请求。 - 默认的
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager没有适配TD Ameritrade的接口特性:它的refresh_token请求默认会携带客户端认证信息,而TD Ameritrade的令牌接口只要求在表单里传client_id,不需要client_secret。
修复步骤
1. 修正application.yml配置
补全TD Ameritrade客户端配置项,关闭不需要的客户端密钥认证,同时增加自定义配置项存储你从TD开发者平台拿到的90天有效期初始refresh_token:
spring: security: oauth2: client: provider: td-ameritrade: token-uri: https://api.tdameritrade.com/v1/oauth2/token reddit: token-uri: https://ssl.reddit.com/api/v1/access_token registration: td-ameritrade: client-id: <<替换为你的client id>>@AMER.OAUTHAP authorization-grant-type: refresh_token # TD接口不校验client_secret,关闭客户端认证 client-authentication-method: none reddit: client-id: <<替换为你的reddit client-id>> client-secret: <<替换为你的reddit client-secret>> authorization-grant-type: client_credentials # 自定义TD配置 td-ameritrade: api-url: https://api.tdameritrade.com/v1/accounts/<<替换为你的账号>>/transactions initial-refresh-token: <<替换为你拿到的90天有效期refresh_token>>
2. 自定义授权逻辑,支持首次用预置refresh_token拉取access_token
修改Oauth2ClientConfiguration类,新增TD专属的授权提供者,补全首次无本地凭据时的令牌请求逻辑,同时适配TD接口的参数要求:
@Configuration @EnableConfigurationProperties(TdAmeritradeConfigurationProperties.class) public class Oauth2ClientConfiguration { @Bean("reddit-client") public WebClient redditWebClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction( authorizedClientManager); oauth.setDefaultClientRegistrationId("reddit"); return WebClient.builder().filter(oauth).build(); } @Bean("td-ameritrade-client") public WebClient tdWebClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction( authorizedClientManager); oauth.setDefaultClientRegistrationId("td-ameritrade"); return WebClient.builder().filter(oauth).build(); } @Bean public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveOAuth2AuthorizedClientService authorizedClientService, TdAmeritradeConfigurationProperties tdAmeritradeConfigProperties) { // 自定义TD专属refresh_token授权提供者 ReactiveOAuth2AuthorizedClientProvider tdRefreshTokenProvider = new ReactiveOAuth2AuthorizedClientProvider() { private final RefreshTokenReactiveOAuth2AuthorizedClientProvider defaultRefreshProvider = buildRefreshProvider(); private RefreshTokenReactiveOAuth2AuthorizedClientProvider buildRefreshProvider() { RefreshTokenReactiveOAuth2AuthorizedClientProvider provider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider(); // 自定义令牌请求参数,适配TD接口要求 WebClientReactiveRefreshTokenTokenResponseClient tokenClient = new WebClientReactiveRefreshTokenTokenResponseClient(); tokenClient.setParametersConverter(grant -> { MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add(OAuth2ParameterNames.GRANT_TYPE, grant.getGrantType().getValue()); params.add(OAuth2ParameterNames.REFRESH_TOKEN, grant.getRefreshToken().getTokenValue()); params.add(OAuth2ParameterNames.CLIENT_ID, grant.getClientRegistration().getClientId()); // 传offline参数每次刷新都返回新的90天有效期refresh_token,避免过期 params.add("access_type", "offline"); return params; }); provider.setAccessTokenResponseClient(tokenClient); return provider; } @Override public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) { String registrationId = context.getClientRegistration().getRegistrationId(); if (!"td-ameritrade".equals(registrationId)) { return Mono.empty(); } // 本地已存有效凭据时直接走默认刷新逻辑 if (context.getAuthorizedClient() != null) { return defaultRefreshProvider.authorize(context); } // 本地无凭据时,用配置里的初始refresh_token构造临时授权客户端,触发首次令牌请求 OAuth2RefreshToken initialRefresh = new OAuth2RefreshToken( tdAmeritradeConfigProperties.getInitialRefreshToken(), Instant.now() ); OAuth2AuthorizedClient tempClient = new OAuth2AuthorizedClient( context.getClientRegistration(), "system-service", null, initialRefresh ); OAuth2AuthorizationContext tempContext = OAuth2AuthorizationContext.withAuthorizedClient(tempClient) .principal(() -> "system-service") .build(); return defaultRefreshProvider.authorize(tempContext); } }; // 组装所有授权提供者 ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder .builder() .clientCredentials() .provider(tdRefreshTokenProvider) .build(); AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); clientManager.setAuthorizedClientProvider(authorizedClientProvider); return clientManager; } }
3. 补全配置属性类
确保TdAmeritradeConfigurationProperties包含对应配置字段:
@ConfigurationProperties(prefix = "td-ameritrade") public class TdAmeritradeConfigurationProperties { private String apiUrl; private String initialRefreshToken; // 补全getter、setter public String getApiUrl() { return apiUrl; } public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; } public String getInitialRefreshToken() { return initialRefreshToken; } public void setInitialRefreshToken(String initialRefreshToken) { this.initialRefreshToken = initialRefreshToken; } }
4. 调整业务代码接口地址
修改ExternalServices里的TD接口调用地址读取逻辑,对应新的配置字段名即可。
运行逻辑说明
配置完成后,第一次调用TD接口时,框架会自动使用你配置的初始refresh_token拉取access_token,之后会自动维护令牌生命周期:access_token(默认30分钟有效期)过期时自动用refresh_token续期,每次续期会拿到新的90天有效期refresh_token,只要服务间隔不超过90天发起一次调用,就不需要手动更新refresh_token,全程无用户交互。
如果使用默认内存版ReactiveOAuth2AuthorizedClientService,服务重启后会自动用配置里的初始refresh_token重新拉取令牌;如果需要避免重启依赖初始refresh_token,可以自行实现持久化版本的ReactiveOAuth2AuthorizedClientService,将令牌存储到缓存或数据库中。
内容的提问来源于stack exchange,提问作者Bruce Horn
相关产品推荐
相关产品推荐

