编程语言

Received fatal alert: handshake_failure(Java)原因与修复

作者: · 最后核实

报错信息javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

SSLHandshakeException: 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,或者中间代理把它去掉了,服务器就选不出对应的虚拟主机,可能直接中止握手。

资料来源

  1. Java Secure Socket Extension (JSSE) Reference Guide
  2. openssl-s_client (OpenSSL 3.0)
  3. keytool (Java SE 21 tool reference)