# Received fatal alert: handshake_failure（Java）原因与修复

> Received fatal alert: handshake_failure 表示客户端和服务器谈不拢 TLS 参数，常见于协议版本、加密套件、SNI 或客户端证书问题。

Source: https://ssl.sh/zh/errors/java-sslhandshakeexception-handshake-failure/
Last updated: 2026-11-10

**`SSLHandshakeException: Received fatal alert: handshake_failure` 的意思是：服务器找不到双方都能接受的握手参数，于是主动结束了 TLS 握手。** 这不是证书信任问题，常见原因是没有共同的协议版本或加密套件、没有发送 SNI，或者服务器要求客户端证书而 Java 客户端没出示。

## 快速修复

定位原因要靠握手日志，再从另一台机器确认服务器接受什么。先拿到握手日志：

```bash
java -Djavax.net.debug=ssl:handshake -jar app.jar 2>&1 | tee tls-debug.log
```

再从另一台机器看服务器接受什么：

```bash
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 配置生成器](https://ssl.sh/zh/tools/tls-config-generator/)生成现代配置。
- `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 检测工具](https://ssl.sh/zh/tools/ssl-checker/)显示只有 TLS 1.0/1.1，或者 `openssl s_client -tls1_2` 失败。
- **修复：** 服务器开启 TLS 1.2 和 TLS 1.3，使用 ECDHE 加 AES-GCM 或 ChaCha20-Poly1305 的套件。套件名可以用[加密套件查询](https://ssl.sh/zh/tools/cipher-suite-lookup/)核对。

### 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](https://ssl.sh/zh/glossary/mtls/) 的服务器，会拒绝不发证书、或证书不是它信任的 CA 签发的客户端。银行、支付渠道的专线接口经常要求双向认证。

- **确认：** `openssl s_client` 列出了可接受的客户端 CA；Java 日志里先有 `CertificateRequest`，随后客户端发出的 `Certificate` 消息是空的。
- **修复：** 配置包含客户端证书和私钥的 keystore：

```bash
java -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStoreType=PKCS12 \
     -Djavax.net.ssl.keyStorePassword=secret -jar app.jar
```

如果客户端证书来自公网 CA，请看 [clientAuth EKU 被移除](https://ssl.sh/zh/errors/client-auth-eku-removed/)：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 没有作用，只会把证书校验也关掉。
