编程语言
Received fatal alert: handshake_failure(Java)原因与修复
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failureSSLHandshakeException: Received fatal alert: handshake_failure 的意思是:服务器找不到双方都能接受的握手参数,于是主动结束了 TLS 握手。 这不是证书信任问题,常见原因是没有共同的协议版本或加密套件、没有发送 SNI,或者服务器要求客户端证书而 Java 客户端没出示。
快速修复
定位原因要靠握手日志,再从另一台机器确认服务器接受什么。先拿到握手日志:
java -Djavax.net.debug=ssl:handshake -jar app.jar 2>&1 | tee tls-debug.log
再从另一台机器看服务器接受什么:
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null
- 两条都失败:服务器只支持 TLS 1.0/1.1 或被 JDK 禁用的老套件,应升级服务器的 TLS 配置,可用 TLS 配置生成器生成现代配置。
openssl能连上而 Java 不行:对照 Java 日志里的 ClientHello 和服务器支持的套件,检查 SNI,再看服务器是否要求客户端证书(openssl输出里有Acceptable client certificate CA names)。
原因
这个告警有 5 种原因,最常见的是双方没有共同的协议版本或加密套件。
1. 没有共同的协议版本或加密套件
当前的 JDK 通过 $JAVA_HOME/conf/security/java.security 里的 jdk.tls.disabledAlgorithms 禁用了 SSLv3、TLS 1.0、TLS 1.1 和 RC4、3DES 等弱算法。只支持这些的服务器会立刻握手失败。对接一些多年没维护的政企、银行前置机、老旧 IIS 接口时最常见。
- 确认: SSL 检测工具显示只有 TLS 1.0/1.1,或者
openssl s_client -tls1_2失败。 - 修复: 服务器开启 TLS 1.2 和 TLS 1.3,使用 ECDHE 加 AES-GCM 或 ChaCha20-Poly1305 的套件。套件名可以用加密套件查询核对。
2. 客户端自己限制了协议或套件
反过来也常见:应用里写死了 https.protocols=TLSv1、jdk.tls.client.protocols,或者多年前的 setEnabledCipherSuites 列表,被新的服务器拒绝。
- 确认: 调试日志里 ClientHello 只提供老版本或很短的套件列表。
- 修复: 删掉写死的协议和套件配置,使用 JDK 默认值。
3. 没有发送 SNI
同一 IP 托管多个域名的服务器(CDN、负载均衡、配了多个 server 的 Nginx)收不到 server_name 扩展时可能直接中止。
- 确认: 日志里 ClientHello 没有
server_name扩展,或者你是用 IP 直连的。 - 修复: 用域名连接,去掉
-Djsse.enableSNIExtension=false,自定义SSLSocketFactory时把主机名传进去。
4. 服务器要求客户端证书
启用了双向 TLS 的服务器,会拒绝不发证书、或证书不是它信任的 CA 签发的客户端。银行、支付渠道的专线接口经常要求双向认证。
- 确认:
openssl s_client列出了可接受的客户端 CA;Java 日志里先有CertificateRequest,随后客户端发出的Certificate消息是空的。 - 修复: 配置包含客户端证书和私钥的 keystore:
java -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStoreType=PKCS12 \
-Djavax.net.ssl.keyStorePassword=secret -jar app.jar
如果客户端证书来自公网 CA,请看 clientAuth EKU 被移除:2026 年起公网 CA 已不再签发可用于客户端认证的 TLS 证书。
5. 密钥类型或签名算法不匹配
服务器只有 ECDSA 证书而客户端禁用了相关曲线,或者服务器证书仍是 SHA-1 签名,也会以这个告警结束。调试日志会写出被拒绝的算法。
各环境修法
各环境的修法都是让客户端和服务器至少有一组共同的 TLS 版本和加密套件,必要时补上 SNI 或客户端证书。
很老的 Java 8: 早期的 Java 8 更新版不支持 TLS 1.3,还可能限制 AES-256 等强加密。升级到 Java 8 的最新更新,或换到仍受支持的 LTS 版本。
Spring Boot / Apache HttpClient / OkHttp: 这些库默认沿用 JVM 的 JSSE 设置,除非你传了自定义协议或套件。在配置里搜 TLSv1、SSLv3 或写死的套件列表。
在负载均衡后面: 握手可能由阿里云 SLB、腾讯云 CLB 等负载均衡终结,而不是 Tomcat。要检查负载均衡上选择的 TLS 安全策略,而不是应用服务器的配置。
容器: 精简 JRE 镜像的 java.security 默认值相同。容器里失败、本机正常,通常是 Java 版本不同,两边都执行 java -version 对比。
不要这样做
全局放开旧协议或关闭校验,会让这个 JVM 的所有连接一起变弱。
- 全局把 TLS 1.0、1.1 从
jdk.tls.disabledAlgorithms里删掉。 这个 JVM 的所有连接都会面临降级攻击。实在绕不开,就把老接口的调用隔离到单独的 JVM,并定下下线时间。 - 从老博客复制一份加密套件列表写死在代码里。 写死的列表很快过时,成为下一次握手失败的原因。
- 装一个全部信任的 TrustManager。 它对 handshake_failure 没有作用,只会把证书校验也关掉。
常见问题
handshake_failure 是证书不受信任吗?
一般不是。证书不受信任时客户端会报 PKIX path building failed。Received fatal alert: handshake_failure 是服务器发来 40 号告警后放弃握手,通常客户端还没看到证书。先查协议版本、加密套件、SNI 和客户端证书。
怎么判断是哪一方的问题?
用 -Djavax.net.debug=ssl:handshake 运行 Java 客户端,看告警前最后几条消息。如果 ClientHello 里只有 TLS 1.2/1.3 的套件,服务器随即回了告警,说明服务器一个都用不了。再用 openssl s_client 和 SSL 检测工具看服务器支持哪些协议,对照即可。
能不能在 Java 里重新启用 TLS 1.0 来连老服务器?
可以改 java.security 里的 jdk.tls.disabledAlgorithms,但这会削弱这个 JVM 的所有连接。只能作为临时、隔离的办法,放在单独的 JVM 上,同时推动对方把服务器升级到 TLS 1.2 或 1.3。
为什么同一个 IP 上只有某个域名握手失败?
多半是 SNI 的问题。客户端用 IP 直连、用 -Djsse.enableSNIExtension=false 关掉了 SNI,或者中间代理把它去掉了,服务器就选不出对应的虚拟主机,可能直接中止握手。