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

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,触发断言异常。

修复步骤

  1. 修改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));
        }
    };
}
  1. 优化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:44:51