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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:07:05