Spring Security+WebClient获取OAuth2令牌时连接超时问题排查
项目背景
基于Spring Boot 3.0.X的Java服务,正在从RestTemplate迁移至WebClient,调用外部系统时遭遇OAuth2令牌请求超时错误,怀疑是代理配置问题。此前存在自定义令牌请求参数(account_id)的需求。
核心配置信息
自定义OAuth2令牌请求要求
令牌请求URI:xxxxxxx/token
请求体格式:
{ "grant_type": "client_credentials", "client_id": "xxxx", "client_secret": "xxxxxxxx", "account_id": "123456789" }
application.yml 配置
security: oauth2: client: registration: custom-client: provider: custom-client client-id: xxxxxxxxxxxxxxx client-secret: xxxxxxxxxxxxxxx authorization-grant-type: client_credentials client-authentication-method: post provider: custom-client: token-uri: xxxxxxxxxxxxxxxxxxxxxxxxxx/token
初始WebClient配置(未添加代理)
@Configuration @Getter @Setter public class WebClientConfig { @Bean public OAuth2AuthorizedClientManager webclientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build(); AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } @Bean public WebClient customWebClient(@Qualifier("webclientManager") OAuth2AuthorizedClientManager authorizedClientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oAuth2Filer = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oAuth2Filer.setDefaultClientRegistrationId("customclient"); return WebClient.builder() .filter(oAuth2Filer) .baseUrl(apiUrl) .build(); } }
业务调用代码(MyService.java)
@Service public class MyService{ WebClient webClient; public MyService(@Qualifier("customClient") WebClient webClient) { this.webClient = webClient; } public String testRequest() { return webClient.get() .uri("xxxxxx") .retrieve() .bodyToMono(String.class) .block(); } }
错误信息
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: I/O error on POST request for "https:/xxxxxxxxxxxxxx/oauth2/token": A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond; nested exception is java.net.SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond"
已尝试的代理配置(未解决)
@Configuration @Getter @Setter public class WebClientConfig { @Bean public OAuth2AuthorizedClientManager webclientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build(); AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } @Bean public WebClient customWebClient(@Qualifier("webclientManager") OAuth2AuthorizedClientManager authorizedClientManager, final @Value("${proxy.url}") String proxyUrl, final @Value("${proxy.port}") String proxyPort, final @Value("${proxy.username}") String proxyUsername, final @Value("${proxy.password}") String proxyPassword) { ServletOAuth2AuthorizedClientExchangeFilterFunction oAuth2Filer = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oAuth2Filer.setDefaultClientRegistrationId("customclient"); Function<String, String> httpsProxyPassword = username -> proxyPassword; HttpClient client = HttpClient.create() .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP) .host(proxyUrl) .port(proxyPort) .password(httpsProxyPassword) .username(proxyUsername)); return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(client)) .filter(oAuth2Filer) .baseUrl(apiUrl) .build(); } }
问题排查与解决方案
1. 核心问题:代理未作用于OAuth2令牌请求
当前配置的代理仅针对WebClient的业务请求,但OAuth2令牌请求是由AuthorizedClientServiceOAuth2AuthorizedClientManager默认使用RestTemplate发起的,不会复用WebClient的代理配置。需要为令牌请求单独配置代理:
方案1:配置带代理的RestTemplate用于令牌请求
@Bean public OAuth2AuthorizedClientManager webclientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService, @Value("${proxy.url}") String proxyUrl, @Value("${proxy.port}") int proxyPort, @Value("${proxy.username}") String proxyUsername, @Value("${proxy.password}") String proxyPassword) { // 配置代理请求工厂 SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUrl, proxyPort)); requestFactory.setProxy(proxy); // 设置代理认证 Authenticator authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(proxyUsername, proxyPassword.toCharArray()); } }; Authenticator.setDefault(authenticator); RestTemplate restTemplate = new RestTemplate(requestFactory); // 自定义令牌响应客户端,添加account_id参数 ClientCredentialsTokenResponseClient tokenResponseClient = new DefaultClientCredentialsTokenResponseClient(); tokenResponseClient.setRestOperations(restTemplate); tokenResponseClient.setRequestEntityConverter(new OAuth2ClientCredentialsGrantRequestEntityConverter() { @Override protected MultiValueMap<String, String> createParameters(OAuth2ClientCredentialsGrantRequest grantRequest) { MultiValueMap<String, String> parameters = super.createParameters(grantRequest); parameters.add("account_id", "123456789"); return parameters; } }); OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials(c -> c.tokenResponseClient(tokenResponseClient)) .build(); AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; }
方案2:用WebClient发起令牌请求,统一代理配置
@Bean public ClientCredentialsTokenResponseClient webClientTokenResponseClient(WebClient.Builder webClientBuilder, @Value("${proxy.url}") String proxyUrl, @Value("${proxy.port}") int proxyPort, @Value("${proxy.username}") String proxyUsername, @Value("${proxy.password}") String proxyPassword) { // 配置带代理的HttpClient HttpClient httpClient = HttpClient.create() .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP) .host(proxyUrl) .port(proxyPort) .username(proxyUsername) .password(username -> proxyPassword)); WebClient webClient = webClientBuilder .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); // 自定义令牌请求逻辑 return request -> { ClientRegistration clientRegistration = request.getClientRegistration(); return webClient.post() .uri(clientRegistration.getProviderDetails().getTokenUri()) .headers(headers -> { headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); }) .body(BodyInserters.fromFormData("grant_type", "client_credentials") .with("client_id", clientRegistration.getClientId()) .with("client_secret", clientRegistration.getClientSecret()) .with("account_id", "123456789")) .retrieve() .bodyToMono(OAuth2AccessTokenResponse.class) .map(response -> OAuth2AccessTokenResponse.withToken(response.getTokenValue()) .tokenType(response.getTokenType()) .expiresIn(response.getExpiresIn()) .build()); }; } // 在OAuth2AuthorizedClientManager中配置该客户端 @Bean public OAuth2AuthorizedClientManager webclientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService, ClientCredentialsTokenResponseClient tokenResponseClient) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials(c -> c.tokenResponseClient(tokenResponseClient)) .build(); AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; }
2. 基础验证步骤
- 检查代理端口参数:当前代码用
String接收proxy.port,需改为int类型,否则端口配置无效 - 用curl验证代理连通性:
curl -x http://${proxy.username}:${proxy.password}@${proxy.url}:${proxy.port} -X POST https://xxxxxxxxxxxxxx/oauth2/token -d "grant_type=client_credentials&client_id=xxxx&client_secret=xxxxxxxx&account_id=123456789" - 确认服务器网络权限:检查应用服务器是否能ping通代理、telnet代理端口,是否有防火墙/VPN限制出站请求
内容的提问来源于stack exchange,提问作者mwhere

