Spring Boot替换OAuth2令牌客户端配置代理遇accessToken空指针问题
问题描述
我们在基于Java 21的Spring Boot Web微服务中,需实现以下需求:
- 通过OAuth2认证调用第三方API
- 调用请求走代理
此前用自定义ClientHttpRequestInterceptor、自定义配置Bean与OAuth2AuthorizedClientManager的组合可正常运行,但Spring弃用DefaultClientCredentialsTokenResponseClient后,改用替代类RestClientClientCredentialsTokenResponseClient出现异常。
新版代码
第三方OAuth请求拦截器
@Component @RequiredArgsConstructor @Slf4j public class ThirdpartyOAuthRequestInterceptor implements ClientHttpRequestInterceptor { private final @Qualifier(THIRDPARTY_AUTH_CLIENT_MGR) AuthorizedClientServiceOAuth2AuthorizedClientManager thirdpartyAuthorizedClientManager; @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest .withClientRegistrationId(ThirdpartyConfiguration.THIRDPARTY_REGISTATION_ID) .principal("OUR SERVICE").build(); OAuth2AuthorizedClient authorizedClient = thirdpartyAuthorizedClientManager.authorize(authorizeRequest); OAuth2AccessToken accessToken = Objects.requireNonNull(authorizedClient).getAccessToken(); request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()); return execution.execute(request, body); } }
第三方配置Bean
@Configuration @RequiredArgsConstructor @Slf4j public class ThirdpartyConfiguration { private final ThirdpartyConfigurationProperties properties; public static final String THIRDPARTY_AUTH_CLIENT_MGR = "thirdpartyAuthorizedClientManager"; public static final String THIRDPARTY_REGISTATION_ID = "thirdparty"; @Bean ClientRegistration thirdpartyClientRegistration(ThirdpartyConfigurationProperties config) { return ClientRegistration .withRegistrationId(THIRDPARTY_REGISTATION_ID) .tokenUri(config.getTokenEndpoint().toString()) .clientId(config.getClientId()) .clientSecret(config.getClientSecret()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .build(); } @Bean public ClientRegistrationRepository clientRegistrationRepository(ClientRegistration thirdpartyClientRegistration) { return new InMemoryClientRegistrationRepository(thirdpartyClientRegistration); } @Bean(THIRDPARTY_AUTH_CLIENT_MGR) public AuthorizedClientServiceOAuth2AuthorizedClientManager thirdpartyAuthorizedClientManager( final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) { final var tokenResponseClient = new RestClientClientCredentialsTokenResponseClient(); RestClient restClient = RestClient.builder(buildRestTemplate(properties)) .baseUrl(String.valueOf(properties.getTokenEndpoint())) .build(); tokenResponseClient.setRestClient(restClient); final var authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(tokenResponseClient); final var authClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); authClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authClientManager; } private RestTemplate buildRestTemplate(ThirdpartyConfigurationProperties config) { HttpHost proxy = new HttpHost(config.getProxyHostname(), config.getProxyPort()); var httpClient = HttpClientBuilder.create() .setRoutePlanner(new DefaultProxyRoutePlanner(proxy)) .build(); var proxyRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); var restTemplate = new RestTemplate(); restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(proxyRequestFactory)); var interceptors = restTemplate.getInterceptors(); interceptors.add(new LoggingInterceptor()); restTemplate.setInterceptors(interceptors); return restTemplate; } public static class LoggingInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept( HttpRequest req, byte[] reqBody, ClientHttpRequestExecution ex) throws IOException { LOG.info("Request body: {}", new String(reqBody, StandardCharsets.UTF_8)); ClientHttpResponse response = ex.execute(req, reqBody); InputStreamReader isr = new InputStreamReader( response.getBody(), StandardCharsets.UTF_8); String body = new BufferedReader(isr).lines() .collect(Collectors.joining("\n")); LOG.info("Response body: {}", body); return response; } } }
错误日志
ERROR 1 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/eed] threw exception [Request processing failed: java.lang.IllegalArgumentException: accessToken cannot be null] with root cause java.lang.IllegalArgumentException: accessToken cannot be null at org.springframework.util.Assert.notNull(Assert.java:181) ~[spring-core-6.2.2.jar:6.2.2] at org.springframework.security.oauth2.client.OAuth2AuthorizedClient.<init>(OAuth2AuthorizedClient.java:78) ~[spring-security-oauth2-client-6.4.2.jar:6.4.2] at org.springframework.security.oauth2.client.OAuth2AuthorizedClient.<init>(OAuth2AuthorizedClient.java:64) ~[spring-security-oauth2-client-6.4.2.jar:6.4.2] at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.4.2.jar:6.4.2] at org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager.authorize(AuthorizedClientServiceOAuth2AuthorizedClientManager.java:144) ~[spring-security-oauth2-client-6.4.2.jar:6.4.2] at this.is.ours.thirdparty.configuration.ThirdpartyOAuthRequestInterceptor.intercept(ThirdpartyOAuthRequestInterceptor.java:38) ~[classes/:1.0.576]
令牌响应日志(正常返回)
Response body: {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJLTWtORUZudjNYYWhLSEk5YmFvc29XYS1rQWmVzb3VyY2VfYWNjZXNzIjp7IndlcnQxNC1hcGkiOnsicm9sZXMiOlsiYWNjZXNzIiwidzE0LWFwaTp2MTpidWlsZGluZ19wcmVmaWxsOnJlcG9ydDplbmVyZ3lfcGVyZm9ybWFuY2VfY2VydGlmaWNhdGVfZHJhZnZmB5PA","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":1662484858,"scope":""}
问题
明明令牌接口返回了正常的access_token,为何会抛出accessToken cannot be null的异常?
解决方案
核心问题是LoggingInterceptor读取响应流后未重置流位置:
Spring Security的RestClientClientCredentialsTokenResponseClient需要读取响应体解析为OAuth2AccessToken,但LoggingInterceptor在记录日志时已将响应流读完,流指针移至末尾,后续RestClient再读取时无法获取内容,导致解析失败,最终返回的令牌为null,触发断言异常。
修复步骤
- 修改LoggingInterceptor,读完响应体后重新包装响应对象,让流可被再次读取:
@Override public ClientHttpResponse intercept( HttpRequest req, byte[] reqBody, ClientHttpRequestExecution ex) throws IOException { LOG.info("Request body: {}", new String(reqBody, StandardCharsets.UTF_8)); ClientHttpResponse response = ex.execute(req, reqBody); // 读取并缓存响应体 String body = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)) .lines() .collect(Collectors.joining("\n")); LOG.info("Response body: {}", body); // 重新构造响应,让后续组件能再次读取流 return new BufferingClientHttpResponseWrapper(response) { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); } }; }
- 优化RestClient构建逻辑(可选):
无需将RestTemplate传给RestClient.builder(),直接用RestClient原生方式配置代理和拦截器,避免组件混用的复杂度:
// 替换原RestClient构建代码 RestClient restClient = RestClient.builder() .requestFactory(() -> { HttpHost proxy = new HttpHost(properties.getProxyHostname(), properties.getProxyPort()); CloseableHttpClient httpClient = HttpClientBuilder.create() .setRoutePlanner(new DefaultProxyRoutePlanner(proxy)) .build(); return new HttpComponentsClientHttpRequestFactory(httpClient); }) .baseUrl(properties.getTokenEndpoint().toString()) // 添加RestClient专属日志拦截器 .interceptors((request, body, execution) -> { LOG.info("Token request body: {}", new String(body, StandardCharsets.UTF_8)); ClientHttpResponse response = execution.execute(request, body); String responseBody = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)) .lines() .collect(Collectors.joining("\n")); LOG.info("Token response body: {}", responseBody); // 重置响应流 return new BufferingClientHttpResponseWrapper(response) { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); } }; }) .build();
内容的提问来源于stack exchange,提问作者tuse-ausm-chor
相关产品推荐
相关产品推荐

