如何在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

