Java Apache HttpClient为何发送两次请求:首次无Basic认证头
问题:HttpClient发送Basic认证POST请求时出现两次请求的原因分析
问题描述
使用Apache HttpClient实现带Basic认证的POST请求,代码如下:
CredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials("user","pass")); RequestConfig requestConfig = RequestConfig.custom(). setConnectTimeout(2000). setSocketTimeout(2000). build(); try (CloseableHttpClient httpClient = HttpClients.custom(). setDefaultCredentialsProvider(provider). setDefaultRequestConfig(requestConfig). build()) { String reqString = mapper.writeValueAsString(obj); // obj为请求体对应的Java类 StringEntity params = new StringEntity(reqString); HttpPost httpPost = new HttpPost(url); // 请求目标服务器地址 httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Content-type", "application/json"); httpPost.setEntity(params); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { final String jsonString = EntityUtils.toString(response.getEntity()); if (200 == response.getStatusLine().getStatusCode()) { mapper = new ObjectMapper(); } } }
执行CloseableHttpResponse response = httpClient.execute(httpPost)时,服务器收到两次请求:第一次请求无Basic Authorization头,第二次带有该认证头。
追踪到httpclient-4.5.3库中,两次请求的生成点在org.apache.http.impl.execchain.MainClientExec类的execute方法内,该方法通过循环执行了两次,核心代码如下:
for (int execCount = 1;; execCount++) { if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) { throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity."); } if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } if (!managedConn.isOpen()) { this.log.debug("Opening connection " + route); try { establishRoute(proxyAuthState, managedConn, route, request, context); } catch (final TunnelRefusedException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage()); } response = ex.getResponse(); break; } } final int timeout = config.getSocketTimeout(); if (timeout >= 0) { managedConn.setSocketTimeout(timeout); } if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } if (this.log.isDebugEnabled()) { this.log.debug("Executing request " + request.getRequestLine()); } if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { if (this.log.isDebugEnabled()) { this.log.debug("Target auth state: " + targetAuthState.getState()); } this.authenticator.generateAuthResponse(request, targetAuthState, context); } if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) { if (this.log.isDebugEnabled()) { this.log.debug("Proxy auth state: " + proxyAuthState.getState()); } this.authenticator.generateAuthResponse(request, proxyAuthState, context); } response = requestExecutor.execute(request, managedConn, context); // The connection is in or can be brought to a re-usable state. if (reuseStrategy.keepAlive(response, context)) { // Set the idle duration of this connection final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { final String s; if (duration > 0) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; } else { s = "indefinitely"; } this.log.debug("Connection can be kept alive " + s); } connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); connHolder.markReusable(); } else { connHolder.markNonReusable(); } if (needAuthentication( targetAuthState, proxyAuthState, route, response, context)) { // Make sure the response body is fully consumed, if present final HttpEntity entity = response.getEntity(); if (connHolder.isReusable()) { EntityUtils.consume(entity); } else { managedConn.close(); if (proxyAuthState.getState() == AuthProtocolState.SUCCESS && proxyAuthState.getAuthScheme() != null && proxyAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting proxy auth state"); proxyAuthState.reset(); } if (targetAuthState.getState() == AuthProtocolState.SUCCESS && targetAuthState.getAuthScheme() != null && targetAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting target auth state"); targetAuthState.reset(); } } // discard previous auth headers final HttpRequest original = request.getOriginal(); if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) { request.removeHeaders(AUTH.WWW_AUTH_RESP); } if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) { request.removeHeaders(AUTH.PROXY_AUTH_RESP); } } else { break; } }
原因分析
这是HttpClient默认采用的**"先尝试无认证请求,再根据服务器401响应补全认证信息重试"**的"质询-响应"认证流程:
- 第一次请求(execCount=1):
初始时targetAuthState状态为UNCHALLENGED,authenticator.generateAuthResponse不会生成Authorization头;请求发送后服务器返回401 Unauthorized响应并携带WWW-Authenticate: Basic头,needAuthentication方法检测到需要认证,更新targetAuthState为CHALLENGED并进入下一次循环。 - 第二次请求(execCount=2):
循环再次执行时,authenticator.generateAuthResponse检测到targetAuthState已处于CHALLENGED状态,会从CredentialsProvider中取出配置的用户名密码,生成Authorization: Basic xxx头添加到请求中;携带认证头的请求发送后,服务器通过认证返回成功响应,循环结束。
如何避免两次请求
如果确定目标服务器必须使用Basic认证,可以强制HttpClient提前发送认证头,无需等待服务器401质询:
// 创建BasicAuthCache并绑定到目标主机 AuthCache authCache = new BasicAuthCache(); BasicScheme basicAuth = new BasicScheme(); HttpHost targetHost = new HttpHost(url.getHost(), url.getPort(), url.getProtocol()); authCache.put(targetHost, basicAuth); // 将AuthCache放入请求上下文 HttpClientContext context = HttpClientContext.create(); context.setAuthCache(authCache); // 执行请求时传入上下文 try (CloseableHttpResponse response = httpClient.execute(httpPost, context)) { // 处理响应逻辑 }
内容的提问来源于stack exchange,提问作者Rasel
相关产品推荐
相关产品推荐

