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

Spring Batch中OAuth2 RestTemplate拦截器servletRequest cannot be null错误解决

问题:Spring Batch中RestTemplate集成OAuth2客户端凭证授权时抛出servletRequest cannot be null异常

我正尝试在Spring Batch中使用自定义ClientHttpRequestInterceptor添加x-access-token请求头,替换已废弃的OAuth2RestTemplate,目标是配置支持OAuth2客户端凭证授权的RestTemplate,同时最小化现有代码改动。

已完成的Bean配置如下:

安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/public/**").permitAll()
                .anyRequest().permitAll())
            .oauth2Client(withDefaults());
        return http.build();
    }
}

客户端注册配置

@Configuration
public class ClientRegistrationConfig {

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(ConfigModel configModel) {
        ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("my-oauth2-client")
                .tokenUri(configModel.getGatewayAuthTokenUri())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientId(configModel.getGatewayAuthClientId())
                .clientSecret(configModel.getGatewayAuthClientSecret())
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scope("openid")
                .build();

        return new InMemoryClientRegistrationRepository(clientRegistration);
    }
}

RestTemplate配置

public class RestTemplateConfig {

    @Bean(name = "gatewayRestTemplate")
    public RestTemplate gatewayRestTemplate(
            RestTemplateBuilder builder,
            OAuth2AuthorizedClientManager authorizedClientManager,
            ClientRegistrationRepository clientRegistrationRepository) {

        ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("my-oauth2-client");
        ClientHttpRequestInterceptor interceptor = new OAuth2ClientInterceptor(authorizedClientManager, clientRegistration);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(10000, TimeUnit.MILLISECONDS)
                .setResponseTimeout(10000, TimeUnit.MILLISECONDS)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .build();

        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
        factory.setConnectTimeout(Duration.ofMillis(13000));

        return builder
                .requestFactory(() -> factory)
                .additionalInterceptors(interceptor) // 添加自定义拦截器设置x-access-token
                .build();
    }
}

自定义拦截器

public class OAuth2ClientInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final ClientRegistration clientRegistration;

    public OAuth2ClientInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().set("x-access-token", getAccessToken());
        return execution.execute(request, body);
    }

    private String getAccessToken() {
        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(clientRegistration.getClientId())
                .build();

        OAuth2AuthorizedClient client = manager.authorize(authorizeRequest);
        Assert.notNull(client, () -> "授权客户端失败,Registration id: '" + clientRegistration.getRegistrationId() + "',返回客户端为null");
        return client.getAccessToken().getTokenValue();
    }
}

问题现象

在Spring Batch任务中调用restTemplate.exchange()时,OAuth2AuthorizedClientManager的authorize()方法抛出异常:servletRequest cannot be null,异常源自DefaultOAuth2AuthorizedClientManager的authorize()方法。

调研过RestClient和WebClient方案,但切换需要大量重构,希望避免,现咨询如何配置OAuth2AuthorizedClientManager或处理该问题,使拦截器方案生效,或提供适用于该场景的替代方案。


解决方案

核心原因

DefaultOAuth2AuthorizedClientManager默认依赖Servlet上下文的HttpServletRequest,但Spring Batch运行在无请求上下文的批处理环境中,因此触发该空指针异常。需要使用不依赖Servlet请求的授权客户端管理器实现。

方案1:配置AuthorizedClientServiceOAuth2AuthorizedClientManager

替换默认的DefaultOAuth2AuthorizedClientManager,使用基于服务存储的实现,适配批处理无上下文场景:

@Configuration
public class OAuth2ClientConfig {

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService) {

        AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);
        // 明确配置客户端凭证授权提供者
        manager.setAuthorizedClientProvider(
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials()
                        .build());
        return manager;
    }
}

方案2:调整拦截器的Principal设置

客户端凭证模式下,OAuth2AuthorizeRequest中的principal仅用于标识客户端,设置任意非空字符串即可,无需使用客户端ID:

private String getAccessToken() {
    OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
            .withClientRegistrationId(clientRegistration.getRegistrationId())
            .principal("batch-task-principal") // 任意非空值均可
            .build();

    OAuth2AuthorizedClient client = manager.authorize(authorizeRequest);
    Assert.notNull(client, () -> "授权客户端失败,Registration id: '" + clientRegistration.getRegistrationId() + "',返回客户端为null");
    return client.getAccessToken().getTokenValue();
}

替代方案:直接使用OAuth2ClientCredentialsClient获取Token

如果不想依赖拦截器,可以直接通过OAuth2ClientCredentialsClient手动获取Token并设置请求头:

@Component
public class BatchTokenProvider {

    private final OAuth2ClientCredentialsClient clientCredentialsClient;
    private final ClientRegistration clientRegistration;

    public BatchTokenProvider(OAuth2ClientCredentialsClient clientCredentialsClient,
                              ClientRegistrationRepository clientRegistrationRepository) {
        this.clientCredentialsClient = clientCredentialsClient;
        this.clientRegistration = clientRegistrationRepository.findByRegistrationId("my-oauth2-client");
    }

    public String getAccessToken() {
        OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
        OAuth2AccessTokenResponse tokenResponse = clientCredentialsClient.getToken(grantRequest);
        return tokenResponse.getAccessToken().getTokenValue();
    }
}

在Batch任务中使用:

@Autowired
private BatchTokenProvider tokenProvider;
@Autowired
@Qualifier("gatewayRestTemplate")
private RestTemplate restTemplate;

public void runBatchJob() {
    String accessToken = tokenProvider.getAccessToken();
    HttpHeaders headers = new HttpHeaders();
    headers.add("x-access-token", accessToken);
    HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

    ResponseEntity<String> response = restTemplate.exchange(
            "https://your-target-api.com/endpoint",
            HttpMethod.GET,
            requestEntity,
            String.class);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:49:58