快速修复
# 客户端全局禁用旧协议(改 java.security,最常用)
# $JAVA_HOME/jre/lib/security/java.security(Oracle)或 $JAVA_HOME/conf/security/java.security(OpenJDK)
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, MD5withRSA, DH keySize < 768
# 只影响当前应用(JVM 启动参数;-Dhttps.protocols 只管 HttpsURLConnection,
# -Djdk.tls.client.protocols 管全局客户端)
-Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
# 服务端侧(nginx)
ssl_protocols TLSv1.2 TLSv1.3;
你会遇到什么
老项目(JDK 1.6/1.7/1.8 早期)升级依赖、迁移服务器或开启 HTTPS 后,HTTPS 调用突然失败:
- 客户端抛
javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version或No appropriate protocol (protocol is disabled or cipher suites are inappropriate); - 服务端
openssl s_client -connect <host>:<port> -tls1能通,但-tls1_2连不上(服务端只支持旧协议);或反过来,新服务端拒绝老客户端。
本文路径:快速修复(30 秒一段命令)→ 一步步排查(可跳读)→ 原理与变体(症状/原因/证据)。赶时间直接跳到「快速修复」。
为什么会这样
TLS 握手要求客户端与服务端的「可用协议集合」交集非空;交集为空时服务端发 alert protocol_version(alert 号 70),Java 侧就是 SSLHandshakeException。三种典型坑:
| 坑 | 机制 |
|---|---|
| 服务端只开 TLSv1.0/1.1 | 新 JDK/OpenSSL 默认已禁旧协议,客户端没有可协商协议 |
| 老 JDK 默认最高 TLSv1.0 | JDK 1.7 及以下默认不启用 TLS1.2,连不上只支持 TLS1.2+ 的新服务端 |
| 代码/配置硬编码旧协议 | setEnabledProtocols("TLSv1")、SSLv3、自定义 SSLContext、跳过主机名验证 |
JDK 各版本默认能力(原文详表摘要):
| JDK 版本 | 默认 TLS 能力 | 关键点 |
|---|---|---|
| 1.6 | 最高 TLSv1.0 | 上 TLS1.2 需换 JDK |
| 1.7.0_79 | 支持 TLS1.2 但默认未启用 | 手动启用 + 禁 TLSv1/1.1 |
| 8u112 | TLS1.2;不支持 TLS1.3 | TLSv1/1.1 默认未禁用,需手动禁 |
| 8u261~8u340 | TLS1.3 服务端默认启用 | 8u341+ 客户端也默认启用 |
| 11.0.0~11.0.10 | TLS1.2/1.3;TLSv1/1.1 未禁 | 11.0.11+ 默认禁 TLSv1/1.1 |
JDK 11 早期 TLS1.3 有会话恢复/死循环 bug(JDK-8211806 不发 SNI、JDK-8212885 证书链丢失、JDK-8213202 竞态、JDK-8214418 100% CPU),11.0.8 前不建议直接用 TLS1.3。
TLS 握手是「协议集合交集」协商:客户端把自己支持的协议列表发给服务端,服务端选交集;交集为空就发 alert protocol_version(70),Java 侧表现为 SSLHandshakeException。
问题永远出在「某一侧的能力集合」与时代脱节:服务端只开 TLSv1.0/1.1(新客户端默认已禁)或客户端太老(JDK ≤1.7 默认最高 TLSv1.0,连不上只支持 1.2+ 的服务端),或代码硬编码旧协议。
JDK 版本默认能力随时间演进(8u112 支持 1.2 不支持 1.3;8u341+ 客户端默认启用 1.3;11.0.11+ 默认禁 1.0/1.1)。
确定性推论:握手失败先确定两侧能力——服务端 openssl s_client -tls1_2 -connect <host>:<port> 测协议;客户端 -Djavax.net.debug=ssl:handshake 看发送的协议列表;修法按侧:服务端开新协议/客户端升级 JDK/代码去掉硬编码协议;
老 JDK 上 1.3 有已知 bug(JDK-8211806 等),11.0.8 前不建议直接用;
判断路径:alert 70 = 协议交集为空,不是证书/密码套件问题。
一步步来
1. 先判断哪边有问题 — 必做
# 对目标 HTTPS 端口三连测:1.2 通 / 1.0、1.1 不通 → 服务端只支持新协议,问题在客户端
openssl s_client -connect <host>:<port> -tls1_2
# 1.0 通 / 1.2 不通 → 服务端还是旧协议,先升级服务端
openssl s_client -connect <host>:<port> -tls1
2. 客户端:禁用旧协议(三种粒度) — 必做
- 全局(推荐):java.security 的
jdk.tls.disabledAlgorithms加TLSv1, TLSv1.1,重启应用;影响该 JDK 上所有应用,若有老应用依赖 TLSv1/1.1 会连失败,先确认。 - JVM 参数(当前应用):
-Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3。 - 代码层(单连接):
connection.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});Spring Boot 用server.ssl.enabled-protocols: TLSv1.2, TLSv1.3。
3. 排查硬编码与跳过校验 — 排查
grep -rn "setHostnameVerifier" --include="*.java" .
grep -rn "TLSv1\|SSLv3" --include="*.java" .
重点:HttpsURLConnection.setEnabledProtocols、Apache HttpClient 的 SSLConnectionSocketFactory 协议列表、RestTemplate 自定义 SSL 上下文、生产环境严禁跳过证书验证的 HostnameVerifier。
4. 验证 — 必做
openssl s_client -connect <host>:<port> -tls1_2 # 应该成功
openssl s_client -connect <host>:<port> -tls1 # 应该失败
openssl s_client -connect <host>:<port> -tls1_1 # 应该失败
应用日志无 SSLHandshakeException / NoSuchAlgorithmException,打印的协议版本为 TLSv1.2。
怎么确认修好了
三连测结果符合「1.2 通、1.0/1.1 拒」,且应用日志握手版本为 TLSv1.2+;Java 侧不再抛 Received fatal alert: protocol_version。
其它环境的表现
| 环境 | 说明 |
|---|---|
| 服务端(nginx/openssl) | ssl_protocols TLSv1.2 TLSv1.3;,等价于实测中的「新服务端」场景 |
| OpenSSL 3.5 | 默认安全级已禁 TLSv1.0/1.1,与 JDK 8u261+/11.0.11+ 默认禁旧协议同机制 |
| 未实测 | JDK 1.6/1.7/8 各版本默认行为表来自原文,本机无老 JDK 未复现 |
症状
HTTPS 调用报 SSLHandshakeException: Received fatal alert: protocol_version 或 No appropriate protocol;openssl 三连测中 TLS1.0/1.1 与 TLS1.2 结果不对称(一边通一边不通)。
原因
客户端与服务端可用 TLS 版本集合交集为空:服务端仅旧协议(TLSv1.0/1.1)、客户端默认禁旧协议;或老 JDK 默认最高 TLS1.0 连新服务端;或代码硬编码旧协议。
解法
- openssl 三连测定位哪边受限(
-tls1/-tls1_1/-tls1_2); - 服务端:nginx
ssl_protocols TLSv1.2 TLSv1.3;,升级老 openssl; - 客户端:java.security
jdk.tls.disabledAlgorithms加TLSv1, TLSv1.1(全局),或-Djdk.tls.client.protocols=TLSv1.2,TLSv1.3(当前应用),或代码setEnabledProtocols(单连接); - 排查并清理
TLSv1/SSLv3 硬编码与跳过校验的 HostnameVerifier; - 三连测验证:1.2 通、1.0/1.1 拒。
验证证据
- 机制实测(2026-08-30,WSL Ubuntu,OpenSSL 3.5.5):本地 TLS1.0-only 服务端 → TLS1.2 客户端握手失败(
unsupported protocol);TLS1.0 客户端成功。反向 TLS1.2-only 服务端 → TLS1.0 客户端报tlsv1 alert protocol version(即 Java 侧Received fatal alert: protocol_version的底层),TLS1.2 客户端成功。证据:实测复现日志(2026-08-30)。 - 未实测:本机无 JDK 1.6/1.7/8,版本默认行为表来自原文《万字长文!TLS协议升级避坑手册》(净重21克,2026-08-26),未在本机复现;OpenSSL 3.5 默认安全级已禁 TLS1.0/1.1,与新版 JDK 默认禁旧协议机制相同。
相关 canonical
按「前置 → 同根 / 同域 / 相关」读:
- 相关(报错相近,根因不同):
- GitHub 直连慢/失败(报错相近(TLS 断连),根因不同)
适用工具 (2)
本问题方案已挂到下列工具的方案页——从工具页也能回到这里,双向可达。
jdk · 外部工具openssl · 外部工具来源与证据
← 安全与治理 问题域 | 问题货架 | 遇到其它问题?去 问题空间 提交求助卡