不使用-Djavax.net.debug如何自定义记录mTLS握手详情
针对你使用Java 17、Spring Boot 2.6.8 + WebClient发起mTLS请求的场景,不需要开启全局javax.net.debug调试参数,有完全可插拔、日志内容/格式/级别完全可控的实现方案,不会产生冗余日志。
方案1:基于Reactor Netty SSL监听实现(推荐,和WebClient过滤器体验一致)
Spring Boot 2.6.x版本默认使用Reactor Netty作为WebClient的底层客户端,你可以在构建WebClient时定制HttpClient的SSL逻辑,注册握手完成事件监听器,直接提取需要的所有握手元数据,完全不侵入原有业务逻辑,也不会修改全局JVM SSL配置。
核心实现代码如下:
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import lombok.extern.slf4j.Slf4j; import reactor.netty.http.client.HttpClient; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509ExtendedTrustManager; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Arrays; @Slf4j public class WebClientMTLSConfig { public WebClient buildMTLSWebClient(X509ExtendedKeyManager keyManager, X509ExtendedTrustManager trustManager) { // 1. 构建SSL上下文,完全复用你原有的密钥库/信任库配置,不改变原有证书自动选择逻辑 SslContext sslContext = SslContextBuilder.forClient() .keyManager(keyManager) .trustManager(trustManager) .sslHandshakeTimeout(Duration.ofSeconds(10)) .build(); // 2. 配置HttpClient,给每个连接绑定握手事件监听器 HttpClient httpClient = HttpClient.create() .secure(sslSpec -> sslSpec.sslContext(sslContext)) .doOnConnected(conn -> conn.channel().pipeline().addLast("mtls-handshake-logger", new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SslHandshakeCompletionEvent handshakeEvent) { try { if (handshakeEvent.isSuccess()) { SslHandler sslHandler = ctx.pipeline().get(SslHandler.class); SSLEngine sslEngine = sslHandler.engine(); SSLSession session = sslEngine.getSession(); // 按需提取所有需要的握手信息 // 提取客户端本次选中发送的证书Subject DN X509Certificate[] clientCerts = session.getLocalCertificates() != null ? Arrays.stream(session.getLocalCertificates()) .map(cert -> (X509Certificate) cert) .toArray(X509Certificate[]::new) : new X509Certificate[0]; String clientCertSubject = clientCerts.length > 0 ? clientCerts[0].getSubjectX500Principal().getName() : "未携带客户端证书"; // 提取协商的TLS版本、加密套件 String tlsVersion = session.getProtocol(); String cipherSuite = session.getCipherSuite(); // 按需提取服务端证书信息、对端地址等 X509Certificate[] serverCerts = (X509Certificate[]) session.getPeerCertificates(); String serverCertSubject = serverCerts.length > 0 ? serverCerts[0].getSubjectX500Principal().getName() : "未知服务端证书"; // 完全自定义日志格式、级别 log.info("mTLS握手完成 | 对端地址:{} | TLS版本:{} | 加密套件:{} | 客户端证书DN:{} | 服务端证书DN:{}", ctx.channel().remoteAddress(), tlsVersion, cipherSuite, clientCertSubject, serverCertSubject); } else { log.error("mTLS握手失败 | 对端地址:{}", ctx.channel().remoteAddress(), handshakeEvent.cause()); } } finally { // 握手只执行一次,处理完移除当前handler,避免连接复用时重复触发 ctx.pipeline().remove(this); } } super.userEventTriggered(ctx, evt); } })); // 3. 构建WebClient,原有过滤器、拦截器逻辑完全兼容 return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) // 可继续添加你现有的ExchangeFilterFunction等过滤器 .build(); } }
这个方案的优势:
- 插拔式配置,仅影响当前WebClient实例,不会修改全局JVM SSL行为,不会产生无关日志
- 完全不干扰原有多证书链自动选择逻辑,只是在握手完成节点读取元数据打日志
- 性能开销极低,监听器仅在握手阶段触发一次,连接复用后无额外消耗
- 日志字段、格式、输出级别完全自主控制
方案2:JDK HttpClient底层适配方案(备选)
如果你将WebClient底层切换为JDK 11+原生HttpClient,可以通过代理X509ExtendedKeyManager的方式埋点:
- 包装你原有的KeyManager实现,重写
chooseClientAlias、chooseEngineClientAlias方法,在客户端选中证书链时记录证书Subject DN、别名等信息 - 给SSLEngine注册
HandshakeCompletedListener,在握手完成回调中提取协商的TLS版本、加密套件、会话信息等内容 - 将包装后的KeyManager、TrustManager注入SSLContext,再绑定到JDK HttpClient,最后传入WebClient构造器即可
这个方案同样是实例级配置,不会产生全局冗余日志,但适配代码比Reactor Netty方案稍繁琐。
不推荐的实现方式
- 不要使用
-Djavax.net.debug=all,handshake全局参数:该配置会打印JVM内所有SSL连接(包括第三方组件发起的HTTPS请求)的调试信息,日志量极大且格式固定无法自定义,生产环境开启很容易打满日志磁盘。 - 不要通过替换全局Security Provider、修改默认SSLContext的方式做埋点:侵入性太高,容易影响应用内其他组件的SSL逻辑,排查问题成本高。
内容的提问来源于stack exchange,提问作者hallo02
相关产品推荐
相关产品推荐

