浏览器不报错的证书链,为什么 curl 和 Java 死活连不上
服务器只发送了叶子证书、没带中间证书时,桌面浏览器可能靠 AIA 补链成功访问,但脚本、Java 应用和不少移动端会直接握手失败。本文解释证书链校验的差异、常见症状,以及如何确认并修复缺失的中间证书。
本文目录

线上经常出现这种场面:同事用浏览器打开站点,小锁亮着,一切正常;运维这边 curl 却报 unable to get local issuer certificate,Java 应用抛出 PKIX path building failed,某些安卓客户端也连不上。两边看的明明是同一个域名,结论却完全相反。
问题通常不在证书本身,而在于服务器只发送了叶子证书,没带中间证书。
校验方拿到的链不一样
TLS 握手里,服务器应该发送从叶子证书到中间证书(不含根证书)的完整链。校验方再从自己信任的根证书出发,逐级验证签名,走到根才算通过。
如果只发叶子证书,校验方手里就有一截断掉的链:它知道这张叶子由某个中间 CA 签发,但本地没有那张中间证书,签名链接不上信任的根,于是失败。
为什么浏览器没事
桌面浏览器会"自救"。很多证书里带有 Authority Information Access 扩展,写明了签发者证书的下载地址。Chrome 和 Firefox 发现链断了,会尝试按这个地址把中间证书抓回来补上,过程对用户完全透明。Firefox 还会缓存见过的中间证书,下次遇到同一张就直接用。
所以浏览器的小锁可能是"补过链之后"的结果,不代表服务器配置正确。
curl、OpenSSL 命令行、Java 的默认 TrustManager、Go 的默认实现,以及大量移动端网络栈,都不做 AIA fetching。链断了就是断了,直接报错。这就是为什么同一个站点会出现"浏览器能开、脚本全挂"的割裂现象。
怎么确认

最直接的检查是看握手时服务器实际发了几张证书:
openssl s_client -connect example.com:443 -servername example.com输出里的 Certificate chain 部分会列出每张证书的主题。健康的情况是从叶子开始依次列出中间证书;如果只列了一张,或者 Verify 输出里出现 unable to get local issuer certificate,基本可以确认缺链。注意 -servername 不能省,SNI 不对可能拿到另一套证书的链。
常见成因和修法
成因几乎都是配置时拼接错了文件:
- 只部署了 CA 返回的叶子证书文件,没拼接 intermediate/bundle。
- Nginx 的
ssl_certificate指令用的是server.crt而不是包含完整链的文件。正确做法是把叶子证书放前面、中间证书按顺序追加在后面,拼成一个文件。 - 换 CA 或续期后只更新了叶子部分,旧的链文件没动——顺带一提,不同 CA 的中间证书不一样,哪怕是同一个 CA,交叉签名场景下也可能有多个有效链。
修完用上面的 s_client 命令复验一遍,确认链完整、Verify return code: 0 (ok),再让之前失败的客户端重试。
一个容易踩的推论
不要拿浏览器作为证书配置的验收标准。它会掩盖缺链问题,让你在部署当晚收到告警——通常是来自那台没人记得的 Java 老服务。验收应该用一个不做补链的客户端,curl 或 openssl s_client 都够用。
在 CertbotX 中实践:自动签发的证书会拿到 CA 返回的完整链,通过 ACME 自动签发并在到期前重新签发时,链文件会随证书一起更新,推送到 Agent、EdgeOne 或 CDN 时也是整链部署,避免"换了叶子忘了换链"这类半截更新。每次部署都有记录可查,出问题时能翻回当时推的是哪条链。