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

如何在Apache HttpClient中记录请求协商的TLS版本?

可行方案梳理

当然可以实现记录协商的TLS版本!而且确实有几种不用大规模修改现有请求构建代码的方案,我给你整理了最实用的几个:

方案1:HttpClient全局响应拦截器(推荐)

这个方案只需要在创建CloseableHttpClient实例时添加一个全局响应拦截器,就能自动捕获所有请求的TLS协商版本,完全不用改动现有请求的构建逻辑。

核心思路是:当请求完成并收到响应后,从当前连接的SSL会话中提取协商好的TLS版本。代码示例如下:

import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.HttpClientContext;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TLSVersionLogger {
    private static final Logger TLS_LOGGER = LoggerFactory.getLogger("TLS_NEGOTIATION");

    public static void main(String[] args) {
        CloseableHttpClient httpClient = HttpClientBuilder.create()
                // 添加响应拦截器,在处理响应时记录TLS版本
                .addInterceptorLast((HttpResponseInterceptor) (response, context) -> {
                    HttpClientContext clientContext = HttpClientContext.adapt(context);
                    ManagedHttpClientConnection conn = clientContext.getConnection(ManagedHttpClientConnection.class);
                    // 判断是否是SSL连接
                    if (conn instanceof SSLConnectionSocketFactory.SSLManagedHttpClientConnection) {
                        SSLSession sslSession = ((SSLConnectionSocketFactory.SSLManagedHttpClientConnection) conn).getSSLSession();
                        if (sslSession != null) {
                            String negotiatedTLS = sslSession.getProtocol();
                            TLS_LOGGER.info("请求URL: {}, 协商TLS版本: {}", 
                                    clientContext.getRequest().getRequestLine().getUri(),
                                    negotiatedTLS);
                        }
                    }
                })
                .build();

        // 你的现有请求代码直接使用这个httpClient即可,无需修改
        // 比如:httpClient.execute(new HttpGet("https://example.com"));
    }
}

这个方案的优点:

  • 全局生效,所有通过该HttpClient发起的请求都会自动记录TLS版本
  • 完全不侵入现有请求构建逻辑,只需要初始化一次HttpClient
  • 可以灵活扩展,比如同时记录请求URL、时间等额外信息

方案2:VM级SSL调试日志(临时调试用)

如果只是临时排查问题,不想写代码,可以直接通过JVM启动参数开启SSL握手日志,日志中会包含协商的TLS版本细节。

只需要在启动应用时添加以下参数:

-Djavax.net.debug=ssl:handshake

启动后,控制台或日志文件会输出大量SSL握手相关的日志,你可以在其中搜索类似以下内容的行,找到最终协商的TLS版本:

Protocol version: TLSv1.3
TLS handshake finished: SSL-Session:
Protocol : TLSv1.2

缺点:日志量非常大,会包含大量SSL握手的细节信息,不适合长期在生产环境启用,仅用于临时调试。

方案3:自定义SSLContext与SSLSocketFactory(进阶场景)

如果你的项目已经自定义了SSLContext(比如用于自定义证书信任),可以通过扩展SSLSocketFactory来捕获TLS会话信息。不过这个方案相比拦截器需要修改SSLContext的配置,复杂度稍高,适合需要更精细控制SSL连接的场景。

核心思路是在SSLSocket握手完成后,从SSLSession中提取TLS版本并记录。示例代码大致如下:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingSSLSocketFactory extends SSLSocketFactory {
    private final SSLSocketFactory delegate;
    private final Logger logger = LoggerFactory.getLogger(LoggingSSLSocketFactory.class);

    public LoggingSSLSocketFactory(SSLSocketFactory delegate) {
        this.delegate = delegate;
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        SSLSocket sslSocket = (SSLSocket) delegate.createSocket(s, host, port, autoClose);
        logTLSVersion(sslSocket, host, port);
        return sslSocket;
    }

    // 实现其他SSLSocketFactory的抽象方法,全部委托给delegate实例
    @Override
    public String[] getDefaultCipherSuites() {
        return delegate.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return delegate.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
        logTLSVersion(sslSocket, host, port);
        return sslSocket;
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
        logTLSVersion(sslSocket, host.getHostAddress(), port);
        return sslSocket;
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localHost, localPort);
        logTLSVersion(sslSocket, host, port);
        return sslSocket;
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        SSLSocket sslSocket = (SSLSocket) delegate.createSocket(address, port, localAddress, localPort);
        logTLSVersion(sslSocket, address.getHostAddress(), port);
        return sslSocket;
    }

    private void logTLSVersion(SSLSocket sslSocket, String host, int port) {
        sslSocket.addHandshakeCompletedListener(event -> {
            SSLSession session = event.getSession();
            logger.info("与 {}:{} 协商的TLS版本: {}", host, port, session.getProtocol());
        });
    }
}

然后将这个自定义的LoggingSSLSocketFactory配置到HttpClient中即可。

总结
  • 日常生产环境记录TLS版本:优先选择全局响应拦截器方案,无侵入且灵活
  • 临时调试排查问题:使用VM级SSL日志,无需写代码
  • 已有自定义SSLContext的场景:可以扩展SSLSocketFactory实现记录

内容的提问来源于stack exchange,提问作者Brad Parks

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 06:23:45