浏览器明明能打开,Java 却说"我不认识这张证书"
一、事情是从一串红字开始的
前两天我在给系统加一个小功能:贴一个 PDF 链接,点「下载并缓存」,服务端用 Java 把文件拉下来、提取文字、再生成文档差异报告。
本地上传一直好好的。我随手贴了个 https://www.soso.com.tw/... 的链接,点下去,页面弹出一坨红字:
PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target第一反应是网络问题:URL 写错了?超时了?被防火墙挡了?
结果我把同一个链接复制到 Chrome、Edge,又找同事在外网、国外都试了一遍——全都正常,绿色小锁,页面秒开。
这就很尴尬了。同一个地址,全世界的浏览器都认,就我的 Java 不认。
二、我开始一本正经地瞎猜
后面半小时,我把能想到的都猜了一遍,而且每猜一次都觉得"这次肯定对了":
- 猜是内网自签证书:这么长的域名,看着像内部系统吧?结果被一句话问住:"这是全球都能访问的公网域名,国外浏览器也正常。"
- 猜是公司 HTTPS 代理在中间替换证书:代理那张 CA 只装在员工电脑上,没进 Java。听起来特别合理。
- 猜是证书链没发全:浏览器会自动顺着证书里的地址把缺的中间证书补下载,Java 默认不补。这个解释我几乎信了,因为"浏览器行、Java 不行"简直是它的标准症状。
- 猜是 JDK 太老、根证书过期。
甚至还把「中间 CA(证书链中间那一层)」和「中间代理(网络设备)」两个词搅在一起,越想越乱。
现在回头看,问题就出在:我一直在猜,没去看一眼那条链到底长什么样。
三、别猜了,直接问服务器
后来我跑了三条命令,事情在两分钟内就清楚了:
keytool -printcert -sslserver www.soso.com.tw:443
keytool -list -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass changeit
java -XshowSettings:properties -version第一条把服务器实际发来的证书链摊开给我看,人家发得很完整,一共 4 张:
#0 *.soso.com.tw ← 由 TWCA SSL Certification Authority 签发
#1 TWCA SSL Certification Authority ← 由 TWCA CYBER Root CA G3 签发
#2 TWCA CYBER Root CA G3 ← 由 TWCA CYBER Root CA 签发
#3 TWCA CYBER Root CA ← 由 TWCA Global Root CA 签发最后那位担保人叫 TWCA Global Root CA(台湾的一家公共 CA)。根证书按惯例不由服务器发送,客户端得自己认识它。
第二条命令,我去翻我电脑上的"信任名单":
| 我机器上的 JDK | 名单里有 TWCA 吗 |
|---|---|
jdk-17.0.5(项目实际在用的) | 没有 |
jdk-24(命令行里那个 java,112 张根证书) | 有,twcaglobalrootca |
第三条命令则提醒了我一件容易忽略的事:机器上装了好几个 JDK,java -version 显示的是 PATH 里那个 24,而 JAVA_HOME 指向 17,真正跑应用的是后者。我一直在查错的那个 JDK 的名单。
四、哦,原来是这么回事
打个比方就特别好懂:
有人拿着一串介绍信来办事——"我是这个网站,A 替我担保,A 由 B 担保,B 由 C 担保,C 由 TWCA 担保"。Java 翻出自己那本信任名单小本本,从头翻到尾,没有 TWCA 这个名字,于是客客气气地回一句"抱歉,这条链我不认"。
浏览器手里那本名单厚得多,上面有 TWCA,所以浏览器说"认识,放行"。
两个都没撒谎,只是它们拿的压根不是同一本名单。浏览器用 Chrome / Mozilla / Apple / Windows 各自的根证书计划,Java 用自己那份 $JAVA_HOME/lib/security/cacerts,Oracle 维护的清单比浏览器精简不少,区域性 CA 在旧版 JDK 里尤其容易缺席。
还有个细节让我一下想通了"为什么偏偏是现在":这张服务器证书是 11 月 1 日刚换的,换成了 TWCA 的新链。之前那位"担保人"在 Java 的名单里,换成 TWCA 之后就不认识了。所以不是我改坏了代码,是对面悄悄换了个担保人。
至于 unable to find valid certification path 这句英文,翻成大白话就是"我顺着链往上找,没找到一个我信得过的人"——它不等于"证书是假的",也不等于"链没发全",更不等于"网络不通"。
五、我是怎么收场的
短期先让功能能跑:加了个开关 pdf.fetch-insecure-ssl: true,让「下载用的那个 HttpClient」暂时跳过证书校验。这里我特意做了两点约束:只影响这一个下载器,不动 JVM 全局 SSL;启动时打一条 WARN 日志,配置里留了 TODO,免得哪天忘了关。
顺手还把报错文案改了——把那一长串英文异常换成中文人话,直接告诉后来的人"大概率是运行所用 JDK 的 cacerts 没收录这个根 CA,以及该怎么查",别再像我对着 PKIX 发半小时呆。
但心里清楚,跳过校验是止痛药,不是药。真正的解法是把那位"担保人"加进名单。巧的是本机 JDK 24 里已经有这张根证书了,直接导出来再导进 JDK 17 就行,连外网都不用去翻:
"D:\Java\jdk-24\bin\keytool.exe" -exportcert -alias twcaglobalrootca -keystore "D:\Java\jdk-24\lib\security\cacerts" -storepass changeit -file twca-root.cer
"D:\Java\jdk-17.0.5\bin\keytool.exe" -importcert -alias twcaglobalrootca -file twca-root.cer -keystore "D:\Java\jdk-17.0.5\lib\security\cacerts" -storepass changeit重启,把开关改回 false,这坑就算填上了。
六、几句话总结这次的教训
- "浏览器能打开"证明不了"程序也能打开",两套信任库各管各的,这个隐含假设坑了我最久。
- 别猜,先看链。
keytool -printcert -sslserver 域名:443一条命令就能看清是自签、链不全,还是单纯缺根证书。 - 先搞清楚应用到底跑在哪个 JDK 上。一台机器装三四个 JDK 太常见了,
java -version和JAVA_HOME未必是同一个。 - 报错说"找不到有效证书路径",重点在"找不到信任锚",不要条件反射地往代理、自签、防火墙上想。
- 跳过校验一定要留痕迹:限定作用域、打 WARN、写 TODO。不然半年后没人记得这里为什么"不校验",就真成隐患了。
- 最后一条,也是最有用的一条:证书是会被换掉的。今天好好的接口,明天可能就因为对方换了一张新链而报错,跟你的代码一点关系都没有。