编程语言
No appropriate protocol(Java)原因和修法
javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) 的意思是:Java 手里已经没有可以使用的 TLS 版本了。 你的代码启用的每一个协议,要么被 JDK 安全策略禁用,要么没有可用的加密套件。这个异常在本地发生,Java 甚至还没联系服务器;最常见的原因是代码或配置写死了 TLS 1.0 或 1.1。
这条报错是什么意思
握手开始时,JSSE 会算出一个“可用协议”列表:在启用的协议(来自 SSLContext、setEnabledProtocols、jdk.tls.client.protocols 或某个库的配置)里,去掉被 jdk.tls.disabledAlgorithms 禁用的,再去掉没有任何可用加密套件的。列表为空,HandshakeContext 就抛出这条异常,ClientHello 不会发出去。
从 JDK 8u291(2021 年 4 月 20 日)、11.0.11 和 16 开始,jdk.tls.disabledAlgorithms 里包含了 TLSv1 和 TLSv1.1。只请求这两个版本的代码,现在得到的就是一个空列表。新版 JDK 禁用的东西更多:Oracle 加密路线图写明,JDK 24 及以后禁用了 TLS_RSA 加密套件,只指定这类套件的代码也会以同样的方式失败。
有一条相近的报错要区分:客户端启用了 TLS 1.2/1.3,但服务器只能用 TLS 1.0,握手会走得更远,然后报 The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]。报错不同,要修的一方也不同。
这个报错常在升级 JDK 之后出现,因为禁用 TLS 1.0/1.1 是通过补丁版本下发的,不需要换大版本。很多团队只是把 JDK 8 升到新的小版本,或者把基础镜像换成新标签,代码一行没改,就开始报这个错。排查时先对比出问题前后的 java -version 输出,基本就能确认。
常见原因(按出现频率排序)
多数情况下是代码或依赖库写死了 TLSv1 或 TLSv1.1。
1. 代码或依赖库写死了 TLSv1 / TLSv1.1
典型写法:SSLContext.getInstance("TLSv1")、socket.setEnabledProtocols(new String[] {"TLSv1"})、HTTP 客户端构造时传入协议数组、-Dhttps.protocols=TLSv1、邮件属性 mail.smtp.ssl.protocols=TLSv1,或者老版本 JDBC 驱动的连接参数限制了 TLS 版本。
- 确认: 搜索代码和配置:
grep -rnE "TLSv1([^.]|\.1)|SSLv3|https\.protocols|ssl\.protocols|enabledTLSProtocols|tlsVersions" src/ config/ *.properties *.yml 2>/dev/null
再用 -Djavax.net.debug=ssl:handshake 跑一次,找 No available cipher suite for ... 和被忽略的协议。
- 修复: 删掉写死的协议,改用
SSLContext.getInstance("TLS"),让 JDK 启用它支持的最新版本(当前版本为 TLS 1.3 和 1.2)。升级写死 TLS 1.0 的老驱动和老依赖。
2. Java 服务器只配置了老协议
Tomcat、Jetty 或 Spring Boot 里配了 server.ssl.enabled-protocols=TLSv1 之类的设置,JDK 一升级,所有客户端连进来都会在服务器端失败。
- 修复: Spring Boot 用
server.ssl.enabled-protocols=TLSv1.3,TLSv1.2;Tomcat 在SSLHostConfig的protocols属性里只保留TLSv1.2和TLSv1.3。
3. 指定的加密套件被禁用或不匹配
“cipher suites are inappropriate”指的是代码把启用的加密套件限定成了一个全都用不了的列表:JDK 24+ 上只有 TLS_RSA_*、只有 3DES 或 RC4,或者只有 TLS 1.3 套件却没启用 TLS 1.3。可以在加密套件查询里查每个套件。
- 修复: 不要在应用代码里指定加密套件,或者改成现代的 ECDHE AEAD 套件。
4. 加固过的或自定义的 java.security
公司的基础镜像、FIPS 配置或 -Djava.security.properties 文件,可能禁用了比默认更多的东西。
- 确认:
grep -n -A4 "^jdk.tls.disabledAlgorithms" "$JAVA_HOME/conf/security/java.security" # JDK 9+
grep -n -A4 "^jdk.tls.disabledAlgorithms" "$JAVA_HOME/jre/lib/security/java.security" # JDK 8
ps -ef | grep -o "java.security.properties=[^ ]*"
分平台修复命令
各平台的修法都是让 JVM 启用 TLS 1.2 和 TLS 1.3:去掉写死的旧协议,或者用 JVM 参数指定新版本。
任意系统,客户端 JVM 参数:
java -Djdk.tls.client.protocols=TLSv1.3,TLSv1.2 -Dhttps.protocols=TLSv1.3,TLSv1.2 -jar app.jar
代码:
SSLContext ctx = SSLContext.getInstance("TLS"); // 不要写 "TLSv1"
ctx.init(null, null, null); // 默认信任库,校验保持开启
Spring Boot(application.properties):
server.ssl.enabled-protocols=TLSv1.3,TLSv1.2
Windows 服务: JVM 参数一般在服务包装程序的配置里(Tomcat 是 tomcat9w.exe 的 Java 选项卡,或服务的 JAVA_OPTS),把同样的 -D 参数加进去。
对方系统确实只支持 TLS 1.0: 正确做法是升级对方系统。作为临时的、范围受限的例外,只给这一个 JVM 加一个覆盖文件:
# /opt/app/legacy-tls.security
# 从你的 JDK 的 java.security 里原样复制 jdk.tls.disabledAlgorithms 的值,
# 只删掉其中的 "TLSv1," 和 "TLSv1.1,",其余条目全部保留。
jdk.tls.disabledAlgorithms=SSLv3, DTLSv1.0, RC4, DES, <...你的 JDK 列表的其余部分...>
java -Djava.security.properties=/opt/app/legacy-tls.security -jar app.jar
只写一个 = 时,这个文件追加到主 java.security 之后,并对自己定义的键生效,而且只影响这个进程。
国内常见场景: 对接多年未升级的老系统、老版本数据库或前置机时最容易遇到,往往是当年为了兼容对方写死了 TLSv1。先确认对方现在是否已支持 TLS 1.2(用下面的 openssl s_client -tls1_2 测一下),很多时候对方早就支持了,只需要删掉自己代码里的旧配置。
不推荐:关闭证书校验
关闭证书校验对这个错误没有用,因为握手还没开始。安装“信任所有证书”的 TrustManager、设置永远返回 true 的 HostnameVerifier、往 cacerts 里随便导入证书,只会让应用接受伪造证书,把账号密码和数据暴露给链路上的任何人。
也不要为了去掉 TLSv1 而修改 JDK 全局的 java.security,那样这台机器上所有 Java 应用都会重新接受已废弃的协议。更安全的做法依次是:删掉写死的协议;推动对方升级到 TLS 1.2+;实在避免不了,才用只作用于单个 JVM、写明删除日期的覆盖文件。
怎么确认已经修好
修好的标准是:用和应用相同的 JDK 和 JVM 参数运行下面的程序,输出 TLSv1.3 或 TLSv1.2。
// TlsCheck.java —— JDK 11+ 可直接运行:java TlsCheck.java example.com
import javax.net.ssl.*;
public class TlsCheck {
public static void main(String[] args) throws Exception {
SSLSocketFactory f = SSLContext.getDefault().getSocketFactory();
try (SSLSocket s = (SSLSocket) f.createSocket(args[0], 443)) {
SSLParameters p = s.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS"); // 保持域名校验
s.setSSLParameters(p);
s.startHandshake();
System.out.println(s.getSession().getProtocol() + " " + s.getSession().getCipherSuite());
}
}
}
这个程序不依赖第三方库,适合在服务器上快速验证,输出里还会带上协商出的加密套件名。openssl s_client -connect example.com:443 -servername example.com -tls1_2 可以在 Java 之外确认服务器接受 TLS 1.2。公网服务器支持哪些版本可以用 SSL 检测工具查看;如果对端归你管,用 TLS 配置生成器生成现代配置。握手能继续往下走、但又报证书错误时,接着看 PKIX path building failed。
常见问题
这个错误是说服务器太老了吗?
不完全是。Java 在联系服务器之前就抛出了它,因为套用安全策略之后,自己能用的协议和加密套件列表已经空了。常见情况是多年前为了对接某台老服务器,有人在代码或配置里写死了 TLSv1。启用 TLS 1.2 后如果服务器仍然不支持,报错会变成 protocol_version 告警或 client preferences 那条。
哪些 JDK 版本禁用了 TLS 1.0 和 1.1?
Oracle 的发布说明里,JDK 8u291(2021 年 4 月 20 日发布)和 11.0.11 包含这项变更;OpenJDK 的 JDK-8202343 随 JDK 16 发布。做法是把这两个协议加进 java.security 的 jdk.tls.disabledAlgorithms;同年 RFC 8996 也正式废弃了它们。
能不能只为一个老系统重新启用 TLS 1.0?
万不得已可以,而且只针对一个 JVM:启动时加 -Djava.security.properties=/path/legacy-tls.security,文件里放一份去掉 TLSv1 的 jdk.tls.disabledAlgorithms。这个文件只对该进程追加并覆盖主 java.security。要定好删除时间,TLS 1.0 已被 RFC 8996 废弃。
怎么看 JVM 忽略了哪些协议和加密套件?
加 -Djavax.net.debug=ssl:handshake 运行。JSSE 调试日志会打印 Ignore unsupported cipher suite: ... for TLSv1.2、No available cipher suite for TLSv1.3 这样的行,能清楚看到每个协议为什么被排除。