Apache HttpClient5.1+Java11 TLS握手缺失server_name扩展致连接重置
问题背景
- 技术栈:Apache HttpClient 5.1 + Java 11,对接开启双向SSL认证的服务端API
- 故障现象:加载TrustMaterial构建SSL上下文、初始化CloseableHttpClient发起GET请求时,持续抛出
Connection Reset错误 - 初步定位:通过配置
System.setProperty("javax.net.debug","ssl,handshake")开启SSL握手调试日志,发现TLS握手发出的ClientHello消息extensions字段缺失*server_name(SNI)*扩展,判断为连接失败核心原因
问题复现代码
public class ClientCustomSSL { public final static void main(final String[] args) throws Exception { System.setProperty("javax.net.debug","ssl,handshake"); File file = new File("C:\\Mycerts.jks"); final SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(file,"passcode".toCharArray()).build(); final SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslcontext).build(); final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(sslSocketFactory) .build(); try (CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(cm).build()) { final HttpGet httpget = new HttpGet("https://mysite-s123/"); final HttpClientContext clientContext = HttpClientContext.create(); try (CloseableHttpResponse response = httpclient.execute(httpget, clientContext)) { System.out.println(response.getCode() + " " + response.getReasonPhrase()); } } } }
关键错误日志
日志核心特征:
- 明确提示SNI扩展生成失败:
Unable to indicate server name、Ignore, context unavailable extension: server name - 最终抛出连接重置异常:
java.net.SocketException: Connection reset
javax.net.ssl|WARNING|01|main|2022-07-08 14:39:57.291 EDT|ServerNameExtension.java: 261 | Unable to indicate server name javax.net.ssl|DEBUG|01|main|2022-07-08 14:39:57.291 EDT|SSLExtensions.java: 260|Ignore, context unavailable extension: server name javax.net.ssl|WARNING| 01|main|2022-07-08 14:39:57.293 EDT|SignatureScheme.java:295|Signature algorithm, ed25519, is not supported by the underlying providers javax.net.ssl|WARNING|01|main|2022-07-08 14:39:57.293 EDT|SignatureScheme.java: 295|Signature algorithm, ed448, is not supported by the underlying providers javax.net.ssl|INFO|01|main|2022-07-08 14:39:57.297 EDT|AlpnExtension.java:178|No available application protocols javax.net.ssl|DEBUG|02|main|2022-07-08 14:39:57.297 EDT|SSLExtensions.java: 260| Ignore, context unavailable extension: application layer protocol negotiation javax.net.ssl|DEBUG|01|main|2022-07-08 14:39:57.297 EDT|SSLExtensions.java:260|Ignore,context unavailable extension: renegotiation_info javax.net.ssl|DEBUG|01|main|2022-07-08 14:39:57.299 EDT|ClientHello.java: 642 Produced ClientHello handshake message "ClientHello":{ "client version":"TLSv1.2", "random":"E1 A8 CF DD 06 87 B9 FD 6C EB FE C6 BC E3 C4 7F EE DC 22 B3 B7 AC 40 0A D0 53 7F 81 51 5D 78 D9", "session id":"E1 A8 CF DD 06 87 B9 FD 6C EB FE C6 BC E3 C4 7F EE DC 22 B3 B7 AC 40 0A D0 78 D9", "cipher suites":"[TLS_AES_256_GCM_SHA384(0x1302),TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B), TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xCCA9)...]", "compression methods": "00", "extensions": [ "status request (5)":{ "certificate status type": ocsp "OCSP status request": { "responder id": <empty> "request extensions":{ <empty> } } }, "supported_groups (10)": { "versions": [x25519, secp256r1, secp384r1, secp521r1, x448, ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192] }, "ec point formats (11)": { "formats": [uncompressed] }, "signature algorithms (13)": { "signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, dsa_sha1] }, "signature algorithms_cert (50)": { "signature schemes" : [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256,dsa_sha1] }, "status request_v2 (17)": { "cert status request":{ "certificate status type": ocsp multi } ] } javax.net.ssl|WARNING|01|main|2022-07-07 09:26:16.233 EDT| SSLSocketImpl.java: 1648| handling exception ("throwable" : { java.net.SocketException: Connection reset at java.base/java.net.SocketInputStream.read(SocketInputStream.java:186) at java.base/java.net.SocketInputStream.read (SocketInputStream.java: 140) at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:478) at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:472) at java.base/sun.security.ssl.SSLSocketInputRecord.decode(SSLSocketInputRecord.java:160) at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:111) at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1501) at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1411) at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:451) at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:422) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:436) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) at ClientCustomSSL.main(ClientCustomSSL.java:62)}
排查步骤与修复方案
SNI扩展默认不生效,按以下顺序排查:
- 检查JVM全局配置
确认没有配置启动参数-Djsse.enableSNIExtension=false,该参数默认值为true,如果被手动改为false会全局禁用SNI。 - 补全SSL连接工厂配置
当前自定义SSLConnectionSocketFactory时仅传入了SSL上下文,缺失主机名验证与TLS细节配置,导致JSSE层拿不到目标主机名,无法生成SNI扩展。修改连接工厂构建代码:import org.apache.http.ssl.DefaultTlsDetailsFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; final SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslcontext) // 显式配置默认主机名验证器 .setHostnameVerifier(SSLConnectionSocketFactory.getDefaultHostnameVerifier()) // 配置TLS细节工厂,确保握手阶段能获取到目标主机名 .setTlsDetailsFactory(new DefaultTlsDetailsFactory()) .build(); - 短域名/IP直连场景显式指定SNI主机名
如果调用的地址是内部短域名(比如示例里的mysite-s123)或者IP直连,Java默认不会为非完全限定域名生成SNI扩展,需要在请求上下文里显式指定目标虚拟主机:final HttpClientContext clientContext = HttpClientContext.create(); // 替换为服务端实际配置证书对应的完整域名 clientContext.setTargetHost(new HttpHost("mysite-s123.your-company.com", 443, "https")); - 验证修复结果
重启应用发起请求,观察SSL调试日志,如果ClientHello的extensions列表中出现server_name字段,且不再抛出Connection Reset错误,说明配置生效。
内容的提问来源于stack exchange,提问作者Knight
相关产品推荐
相关产品推荐

