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

Reactive Spring:如何将OAuth2授权码流的AccessToken传入定时任务

问题描述

我正在构建一个定期从第三方API拉取数据的响应式应用,该API要求通过带PKCE的OAuth 2.0授权码流程完成认证(流程受reCAPTCHA保护)。我希望基于Spring Security OAuth Client实现,而非手动编码授权协议。

核心目标:

  • 实现定时任务MyScheduler#fetchData,在我手动通过浏览器完成第三方登录认证后,任务能正常调用API;认证前任务需阻塞。
  • 浏览器端的授权已正常工作:访问/showData会重定向到厂商登录页,认证完成后可正常展示数据,但定时任务始终被阻塞。

推测问题根源:当前访问令牌仅绑定到浏览器会话的安全上下文,无法在应用全局共享令牌(含刷新令牌)。

注:第三方API提供了定期调用的专用端点,但强制要求使用授权码流程,不支持客户端凭证模式。

当前类结构:
类图

MyController#showData和MyScheduler#fetchData均调用ApiClient#retrieveData,该方法通过Spring响应式WebClient发起API请求。当前WebClient配置如下:

@Configuration
@EnableWebFluxSecurity
class WebClientConfiguration {

    @Bean
    WebClient webClient(ReactiveClientRegistrationRepository clientRegs, 
            ReactiveOAuth2AuthorizedClientService authClientService) {
        ReactiveOAuth2AuthorizedClientManager authClientManager =
            new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegs, authClientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
            new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
        oauth.setDefaultOAuth2AuthorizedClient(true);
        oauth.setDefaultClientRegistrationId("test");
        return WebClient.builder()
          .filter(oauth)
          .build();
    }
    
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
            ServerOAuth2AuthorizationRequestResolver resolver) {
        http.authorizeExchange()
          .anyExchange()
          .authenticated()
          .and()
          .oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
        return http.build();
    }
    
    @Bean
    public ServerOAuth2AuthorizationRequestResolver pkceResolver(
            ReactiveClientRegistrationRepository repo) {
        DefaultServerOAuth2AuthorizationRequestResolver resolver = 
            new DefaultServerOAuth2AuthorizationRequestResolver(repo);
        resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
        return resolver;
    }
}

解决方案

问题出在AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager默认会关联**用户主体(Principal)**存储令牌,而定时任务运行时没有绑定用户上下文,导致无法获取已授权的客户端实例。要实现全局共享令牌,需要做以下调整:

1. 自定义授权客户端管理器,忽略用户主体

创建一个忽略用户主体的ReactiveOAuth2AuthorizedClientManager,让它始终使用同一个"系统用户"标识来存储和获取令牌,这样定时任务和浏览器端可以共享同一套令牌:

@Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrationRepository,
        ReactiveOAuth2AuthorizedClientService authorizedClientService) {

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager manager =
            new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientService);

    // 配置授权客户端提供者,支持授权码和刷新令牌流程
    OAuth2AuthorizedClientProvider provider = OAuth2AuthorizedClientProviderBuilder.builder()
            .authorizationCode()
            .refreshToken()
            .build();
    manager.setAuthorizedClientProvider(provider);

    // 自定义上下文解析器,返回固定的用户主体标识
    manager.setContextAttributesMapper(context -> {
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(OAuth2AuthorizationContext.PRINCIPAL_NAME_ATTRIBUTE_NAME, "system-scheduler");
        return Mono.just(attributes);
    });

    return manager;
}

2. 修改WebClient配置,使用自定义授权管理器

将原来的authClientManager替换为上面自定义的实例:

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegs, 
        ReactiveOAuth2AuthorizedClientService authClientService,
        ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
            new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth.setDefaultOAuth2AuthorizedClient(true);
    oauth.setDefaultClientRegistrationId("test");
    return WebClient.builder()
            .filter(oauth)
            .build();
}

3. 调整定时任务,等待令牌可用

定时任务运行时需确保能获取到授权令牌,添加响应式等待逻辑,直到用户完成认证:

@Component
class MyScheduler {

    private final ApiClient apiClient;
    private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
    private final ReactiveClientRegistrationRepository clientRegs;

    public MyScheduler(ApiClient apiClient,
                       ReactiveOAuth2AuthorizedClientService authorizedClientService,
                       ReactiveClientRegistrationRepository clientRegs) {
        this.apiClient = apiClient;
        this.authorizedClientService = authorizedClientService;
        this.clientRegs = clientRegs;
    }

    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void fetchData() {
        // 重复检查令牌是否可用,每10秒重试一次
        clientRegs.findByRegistrationId("test")
                .flatMap(clientReg -> authorizedClientService.loadAuthorizedClient("test", "system-scheduler"))
                .repeatWhenEmpty(repeat -> repeat.delayElements(Duration.ofSeconds(10)))
                .flatMap(authorizedClient -> apiClient.retrieveData())
                .block(); // 响应式转阻塞,适配定时任务执行逻辑
    }
}

4. 同步登录后的令牌到系统用户标识

修改OAuth2登录成功处理器,将浏览器认证后的令牌复制到固定的"system-scheduler"标识下,确保定时任务能读取到:

@Bean
ServerOAuth2AuthenticationSuccessHandler successHandler(
        ReactiveOAuth2AuthorizedClientService authorizedClientService) {
    return (authentication, exchange) -> {
        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        String clientId = oauthToken.getAuthorizedClientRegistrationId();
        String originalPrincipal = oauthToken.getName();

        // 读取原用户标识下的授权令牌,存储到系统用户标识下
        return authorizedClientService.loadAuthorizedClient(clientId, originalPrincipal)
                .flatMap(authorizedClient -> {
                    OAuth2AuthenticationToken systemAuth = new OAuth2AuthenticationToken(
                            oauthToken.getPrincipal(),
                            oauthToken.getAuthorities(),
                            clientId
                    );
                    return authorizedClientService.saveAuthorizedClient(authorizedClient, systemAuth);
                })
                .then(ServerResponse.ok().build())
                .doOnSuccess(response -> exchange.getResponse().setStatusCode(HttpStatus.OK));
    };
}

// 更新SecurityWebFilterChain,配置成功处理器
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
        ServerOAuth2AuthorizationRequestResolver resolver,
        ServerOAuth2AuthenticationSuccessHandler successHandler) {
    http.authorizeExchange()
            .anyExchange()
            .authenticated()
            .and()
            .oauth2Login(auth -> auth
                    .authorizationRequestResolver(resolver)
                    .authenticationSuccessHandler(successHandler));
    return http.build();
}

关键原理说明

  • 默认的授权客户端管理器会将令牌与登录用户的Principal绑定,定时任务无用户上下文,无法获取令牌;
  • 通过自定义上下文解析器,强制使用固定用户标识存储令牌,实现浏览器登录与定时任务的令牌共享;
  • 登录成功后将令牌复制到固定标识下,确保定时任务能读取到有效授权信息。

内容的提问来源于stack exchange,提问作者nasezoll

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 15:50:37