Zulu OpenJDK 1.8 连接 MySQL 时遇到 TLS 握手失败问题
在 M1 芯片的机器上搭建 Java 开发环境, 如果要选择 1.8 版本的 JDK, 网上推荐 zulu OpenJDK 的比较多, Oracle 官方没有提供二进制的版本。想着既然能用那就没必要自己去编译,之前试过编译 OpenJDK, 要踩的坑不少, 遂安装之。
今天在启动一个项目的时候, 提示数据库连不上, 从异常类名称初步推断是在建立 SSL 握手阶段遇到协议不匹配的问题了
1 | Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) |
我感到十分诧异, 因为另外一个项目连的是同一个数据库服务器, 只是使用的库不同, 如果是 TLS 协议这种层面的问题, 不应该一个项目正常连接数据库而另外一个项目抛出错误信息。我第一时间想到了 JDBC url 有一个参数 useSSL 可以指定是否使用 TLS 协议建立连接, 果然, 将这个参数改为 useSSL=false 后, 连接正常建立了, 这当然并不奇怪。我又比较了数据库客户端 Dbeaver 的连接参数, 发现了一个跟 TLS 有关的参数 enabledTLSProtocols, 其默认值为 TLSv1,TLSv1.1,TLSv1.2, 我尝试在不改 useSSL 的前提下加入 enabledTLSProtocols=TLSv1,TLSv1.1,TLSv1.2, 结果也可以修复这个问题, 进一步排除, 发现只要加了
enabledTLSProtocols=TLSv1.2 即可。但是, 这些明显都不是最优解, 问题的原因还没有被找到。
我开始比较两个项目的差异。同样都是 Spring Boot 项目, 项目初始化的时候基本都是按照固定的模板搭建和引入依赖的,所以基本上差异很小。这个时候我发现报错的那个项目之前我改造过, 因为有同时连接 MySQL 和 SQL Server 数据库的需求, 我写过一个动态数据源配置, 改变了 Spring 连接数据源的默认行为, 会不会是这个原因呢? 于是我检查了一遍代码, 这边在建立两个 HikariDataSource 对象, 加入到 DynamicDataSource 中… 看到一半, 突然想到, 既然是数据库连不上, 那就写一个只连接数据库, 别的什么都不干的代码, 这样排查起来会方便很多。
Java 中用最少和最依赖最小的代码实现连接数据库, 那自然是借助 JDBC 和 Mysql Connector/J 了
1 | import java.sql.Connection; |
在两个项目中将 main 方法修改为上述代码运行, 结果仍然是一个正常另一个报相同的错误信息无法连接。在确定了两个项目使用的 JDK 相同后, 我基本下定论认为肯定是 Mysql Connector/J 的版本问题了。果然, 查看 IDE 的启动参数发现, 报错的项目指定的 Connector/J 的版本是 mysql-connector-java-8.0.17.jar, 而正常运行的项目指定的版本是更高一点的 mysql-connector-java-8.0.21.jar, 原来是两个项目的 Spring Boot 版本不同, 报错的项目是 2.1.7.RELEASE 依赖的是 8.0.17 版本的 Connector/J 而另一个项目是 2.3.4.RELEASE 依赖的是 8.0.21 版本的 Connector/J。
由于 mysql-connector-java-8.0.21.jar 是 Oracle 提供的开发包, 不方便查看它的源代码去确认它改了什么, 于是我进一步实验, 在 8.0.17 到 8.0.21 这两个版本之间, 一定有一个版本对这个问题做了修复。经过实验得出的结论是: 从 mysql-connector-java-8.0.19.jar 开始, 这个问题不再出现。
一般软件都有版本的 changelog, Connector/J 也不例外。通过搜索引擎找到 Connector/J 8.0.19 的更新日志如下
1 | The allowable versions of TLS protocol used for connecting to the server, when no restrictions have been set using the connection properties enabledTLSProtocols, have been changed to: |
到这里基本上已经算破案了, 我们似乎有理由相信, 这是一个由 Mysql Connector/J 驱动引起的 bug。
然而, 我忽略了一个基本的事实, 那就是没有进行对照实验。这一切的实验环境都是 zulu OpenJDK , 我原来电脑使用的是 Oracle JDK, 服务器 Docker 容器上用的是 OpenJDK, 对于这个有问题的项目, 之前它们都是正常运行的啊!
于是问题的焦点重新回到 zulu OpenJDK 身上, 一开始猜测是 OpenJDK 和 Oracle JDK 的差异, 但服务器容器上也是 OpenJDK 正常运行, 我自己编译了 OpenJDK 的代码, 在 Ubuntu 机器上也是正常连接数据库。经过对 zulu OpenJDK 和自行编译的 OpenJDK 源码的漫长的断点调试和比对, 确定了问题出现在下面这个方法
1 | private static List<ProtocolVersion> getActiveProtocols( |
这个 getActiveProtocols()
方法返回当前所支持的协议, zulu OpenJDK 返回一个空的 list 而自行编译的 OpenJDK 返回 TLSv1.0 和 TLSv1.1。
在下面这段判断中, zulu OpenJDK 都是直接走 continue 导致后面的代码都被跳过了
1 | if (!algorithmConstraints.permits( |
走 continue 的原因则是 algorithmConstraints.permits(EnumSet.of(CryptoPrimitive.KEY_AGREEMENT), 'TLSv1.0', null)
和 algorithmConstraints.permits(EnumSet.of(CryptoPrimitive.KEY_AGREEMENT), 'TLSv1.1', null)
均返回 false。这个 permits 返回的是根据协议有没有被禁用来决定的, 协议被禁用返回 false 走 continue 反之不走 continue 继续往下执行。协议有没有被禁用则是根据 CryptoPrimitive.KEY_AGREEMENT
的值去配置文件查询得到的, CryptoPrimitive.KEY_AGREEMENT
代表的是属性 jdk.tls.disabledAlgorithms
的值, 全局搜索在 jre/lib/security/java.security
文件中发现了这个属性的值, zulu OpenJDK 的如下
1 | jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \ |
而编译的 OpenJDK 的配置如下
1 | jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \ |
很明显可以看到, zulu OpenJDK 将 TLSv1 和 TLSv1.1 都加入了禁用列表, 而在没有指定使用 TLSv1.2 的情况下 Java 获取到的协议恰好只有 TLSv1.0 和 TLSv1.1, 这两个都被禁用了, 当然就没有可以使用的协议建立连接了。
后来我试了另一个 OpenJDK 发行版 Temurin 家的 OpenJDK 也是将 TLSv1.0 和 TLSv1.1 直接拉黑了, 目前从 https://www.injdk.cn/ 下载的大多数的 OpenJDK 发行版还是没有禁用这两个协议的。