编程语言
PKIX path building failed(Java)原因与修复
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested targetPKIX path building failed ... unable to find valid certification path to requested target 的意思是:JVM 没法把服务器证书连到它所加载的信任库里的任何一个根证书。 最常见的情况是服务器没发中间证书、公司代理重新签发了 HTTPS 流量,或者内网 CA 从来没导入过 JDK 的 cacerts。
快速修复
服务器缺中间证书就到服务器上补链,JVM 不信任根证书才用 keytool 导入。先判断是哪一种,不要一上来就导证书:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
- 输出里只有一张证书,结尾是
Verify return code: 21 (unable to verify the first certificate):服务器证书链不完整,到服务器上配置完整证书链,见缺少中间证书。 - 证书链完整,但最上面的
issuer是公司名、代理设备厂商或内网 CA:JVM 不信任这张根证书,导入到报错的那个 JVM 的信任库:
# JDK 9 及以后
keytool -importcert -cacerts -alias company-root -file company-root.pem -storepass changeit -noprompt
# JDK 8
keytool -importcert -keystore "$JAVA_HOME/jre/lib/security/cacerts" -alias company-root -file company-root.pem -storepass changeit -noprompt
导入后要重启应用,信任库只在启动时读一次。
原因
这个报错有 5 种原因,最常见的是服务器没发中间证书。
1. 服务器没发中间证书
Java 不会自己去下载缺失的中间证书,所以 Chrome 能打开的网站,Java 照样可能报错。
- 确认:
openssl s_client -showcerts只显示站点证书,或者证书链补全工具提示缺少中间证书。 - 修复: 让服务器发送站点证书加中间证书(Nginx 用
fullchain.pem,Tomcat、Apache、负载均衡上传完整链)。阿里云、腾讯云 SLB/CLB 上传证书时,“证书内容”要粘贴完整链。
2. 公司代理或安全网关解密了 HTTPS
不少企业的上网行为管理、安全网关会解密 HTTPS,再用自己的私有根证书重新签发。浏览器信任它,是因为 IT 已经把根证书推送到了系统证书库;但 Java 不读系统证书库。
- 确认: 加
-Djavax.net.debug=ssl:handshake运行,或在同一台机器上用openssl s_client看,issuer是代理产品名或公司名。 - 修复: 把代理根证书导入 cacerts;Windows 上也可以用
-Djavax.net.ssl.trustStoreType=Windows-ROOT。
3. 内网私有 CA 或自签名证书
内部接口、Nexus 私服、自建 GitLab 经常用公司自建 CA 签发的证书。
- 修复: 每个 JVM 导入一次内网根证书(不是站点证书);更好的做法是做一个专用信任库,让应用显式加载:
keytool -importcert -keystore app-truststore.p12 -storetype PKCS12 -storepass changeit -alias company-root -file company-root.pem -noprompt
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar
注意 javax.net.ssl.trustStore 是替换 cacerts,自定义信任库里也要包含应用需要访问的公网根证书,否则调用支付宝、微信支付等公网接口时又会报同样的错。
4. JDK 太旧,cacerts 没更新
很老的 JDK 版本里没有新的根证书,CA 切换到新根以后签发的证书就验证不过。
- 修复: 升级到同一大版本的最新更新版,每次更新都会带上新的 cacerts。
5. 应用加载的不是你以为的那个信任库
Tomcat Connector、Spring 配置、Kafka 或 Elasticsearch 客户端都可能单独指定信任库。
- 确认: 加
-Djavax.net.debug=ssl:trustmanager,日志会打印实际加载的信任库和其中的证书。
各环境修法
各环境的做法都是把根证书加进实际报错的那个 JVM 读取的信任库,位置因发行版和部署方式而异。
Debian / Ubuntu 的 OpenJDK: cacerts 由 ca-certificates-java 从系统证书库生成,往系统里加根证书就能同时更新 Java:
sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
CentOS / RHEL / Rocky / 阿里云 Alibaba Cloud Linux: 把根证书放到 /etc/pki/ca-trust/source/anchors/,执行 sudo update-ca-trust;系统 OpenJDK 读的是 /etc/pki/ca-trust/extracted/java/cacerts。
Docker 镜像: 在构建时就把根证书打进镜像,每个容器启动即可用:
COPY company-root.pem /tmp/company-root.pem
RUN keytool -importcert -cacerts -alias company-root -file /tmp/company-root.pem -storepass changeit -noprompt
Windows: 用上面的 Windows-ROOT;或者以管理员身份运行 keytool,因为 Program Files 下的 cacerts 默认不可写。
构建工具: Maven、Gradle 下载依赖时也会报同样的错,见 Maven PKIX path building failed。
不要这样做
下面这些做法会关掉身份校验,或者让导入的证书很快失效。
- 写一个什么都信任的
X509TrustManager(checkServerTrusted为空),或者HostnameVerifier直接返回true。这些从博客复制来的代码会关掉身份校验,而且往往一路带进生产环境。 - 导入站点证书代替根证书。 只是把问题藏起来,下次续期又会报错。
- 删掉 cacerts 或用别的机器、旧 JDK 的文件覆盖。 会丢失新的根证书,而且下次升级 JDK 时你的修改又会被覆盖。
常见问题
Java 的 cacerts 文件在哪里?
JDK 9 及以后是 $JAVA_HOME/lib/security/cacerts,JDK 8 是 $JAVA_HOME/jre/lib/security/cacerts。Debian/Ubuntu 的系统 OpenJDK 链接到 /etc/ssl/certs/java/cacerts,CentOS/RHEL 系链接到 /etc/pki/ca-trust/extracted/java/cacerts。默认密码是 changeit。
证书已经导入了,为什么还报 PKIX path building failed?
多半是报错的程序跑在另一个 JVM 上:IDEA 自带的 JDK、Docker 镜像里的 JDK、PATH 上的另一个 Java。在程序里打印 System.getProperty("java.home"),或看 mvn -v、gradle -v 的输出,再往那个 JVM 的 cacerts 里导入。另外检查启动参数里有没有 -Djavax.net.ssl.trustStore,它会整个替换掉 cacerts。
应该导入站点证书还是根证书?
导入根证书(或公司代理的根证书)。站点证书每次续期都会变,按现在的有效期几个月就要换一次,导入站点证书很快又会报错。如果只是服务器缺中间证书,应该修服务器,而不是在客户端导入任何东西。
Java 能直接用 Windows 证书库吗?
可以。在 Windows 上启动 JVM 时加 -Djavax.net.ssl.trustStoreType=Windows-ROOT,就会信任 Windows 证书库里的根证书。公司 IT 已经通过域策略下发了代理根证书时,这个办法最省事。