Spring Batch中OAuth2 RestTemplate拦截器servletRequest cannot be null错误解决
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

