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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 04:15:55