你给站点装好证书,自己在电脑上打开是小锁,一切正常。第二天客户发消息:「你们网站怎么提示不安全?」你换台电脑、换个浏览器,又是对的,于是开始怀疑是客户那边的问题。
大概率不是客户的问题,是证书链没发全。你的浏览器能验证通过,是因为它顺着链条一层层爬到了自己认识的根证书;对方那台机器缺了中间证书,链在半路就断了,只能判「不受信任」。
这件事最坑的地方在于:它没法靠「我这边正常」来排除。自己看一百遍都正常,客户那边照样报错。要判断,必须去看服务器实际发出去了什么。
一、先说清三个角色:根、中间、叶
HTTPS 证书不是一张,是一条链。三层的分工是这样的:
| 角色 | 谁签的 | 在哪 | 作用 |
|---|---|---|---|
| 根证书(Root CA) | 自己签自己 | 操作系统 / 浏览器内置的信任库,一百多张 | 整条信任链的锚点,私钥离线保管 |
| 中间证书(Intermediate CA) | 由根证书签 | 在你的服务器上,跟站点证书一起发 | 代根签你的证书,这样根可以长期不动 |
| 站点证书(叶证书 Leaf) | 由中间证书签 | 在你的服务器上 | 绑你的域名,就是你在后台申请的那张 |
链条长这样:
你的站点证书(mcstudio.top)
↑ 由它签
Let's Encrypt 中间证书(现在的 R10 / R11,早年叫 R3)
↑ 由它签
ISRG Root X1(根证书,内置在系统信任库里)
关键只有一句:服务器在 TLS 握手时,必须把「站点证书 + 中间证书」一起发出去。根证书不用发,浏览器自己库里有。只发站点证书,链就断了——浏览器没义务、通常也没能力自己去网上下载中间证书补上(桌面版会尝试 AIA 补链,但不可靠,手机和 curl 基本不做这件事)。
二、浏览器验证书的五个动作
看懂这五步,看一眼报错就知道问题在哪:
1)取链。从握手的 Certificate 消息里拿到服务器发来的证书列表。
2)找锚点。从最末一张往上看它的签发者(issuer),在本地信任库里找同名的根。
3)逐级验签。用上级的公钥验证下级的签名,一层层爬到根。任何一层断开,整条链作废。
4)查字段。有效期、用途(必须是 serverAuth)、域名(只看 SAN,不看 CN)。
5)查吊销。OCSP / CRL,看这张证书有没有被作废。
五步对应五种报错,这张表值得存下来:
| 浏览器报错 | 真正的原因 |
|---|---|
NET::ERR_CERT_AUTHORITY_INVALID |
链不完整,或者根证书不在对方信任库里 |
NET::ERR_CERT_DATE_INVALID |
证书过期,或服务器/客户端时间不对 |
NET::ERR_CERT_COMMON_NAME_INVALID |
域名不匹配,最常见是 SAN 里漏了 www |
NET::ERR_CERT_REVOKED |
证书被吊销(私钥泄漏、欠费、误操作) |
SSL_ERROR_BAD_CERT_DOMAIN |
拿到的是别的站点的证书,通常是 SNI 没带上 |
看到第一个,先要把「链不全」和「根不受信」分开——这两件事的修法完全不同,第六、七节分别讲。
三、链不全的六种常见成因
1)只上传了单张证书。CA 后台一般给两个下载入口:「服务器证书」和「证书链 / CA Bundle / fullchain」。很多人只下了第一个。
2)Apache 和 Nginx 的规则不一样。Apache 有 SSLCertificateChainFile 专门放中间证书;Nginx 没有这个指令,中间证书必须手工拼进 ssl_certificate 指向的那个文件里。从 Apache 迁到 Nginx 的人最容易在这里翻车。
3)免费证书的目录挑错了。Let’s Encrypt 会同时生成 cert.pem(只有站点证书)和 fullchain.pem(站点 + 中间)。手动 cp 的时候顺手拿了前面那个。
4)面板里粘贴时只粘了一段。宝塔的 SSL 页面有两个框:证书和私钥。证书那个框里要粘两段——站点证书和中间证书连着粘,中间不要留空行。
5)拼接顺序反了。正确顺序是站点证书在前、中间证书在后。反了以后桌面 Chrome 偶尔能靠 AIA 补链救回来,手机和 curl 直接失败——于是又变成「我这边是好的,客户就是不行」。
6)中间证书自己过期了。你的站点证书还有两个月,中间证书先到期了。这种情况特别隐蔽,因为面板后台只显示站点证书的到期时间。
四、一条命令看清楚:openssl s_client
别猜。直接问服务器要一次握手,看它到底发了几张证书:
openssl s_client -connect mcstudio.top:443
-servername mcstudio.top -showcerts </dev/null 2>/dev/null
| grep -E "^ *[0-9]+ s:|i:"
链完整时长这样,i: 会出现两次:
0 s:CN = mcstudio.top
i:C = US, O = Let's Encrypt, CN = R10
1 s:C = US, O = Let's Encrypt, CN = R10
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
链不全的时候只有第一段:
0 s:CN = mcstudio.top
i:C = US, O = Let's Encrypt, CN = R10
判断口诀:数 i: 的行数。1 行=只有站点证书,链不全;2 行=站点+中间,正常;3 行以上=把根证书也发了,能用但白费握手字节。
有两个细节必须注意:
-servername 不能省。不带 SNI 的握手,服务器不知道你要哪个站点,会回默认站点的证书。用 IP 直连测试会踩同一个坑——测出来「域名不匹配」,白忙一场。
顺手看一下总体验证结果:
echo | openssl s_client -connect mcstudio.top:443
-servername mcstudio.top 2>/dev/null | head -5
要看到 Verify return code: 0 (ok)。如果本地能过、手机上不过,那说明问题在对方信任库,不在链上。
五、怎么修:拼一个 fullchain
Nginx 的规则只有一条:ssl_certificate 指向的文件,第一张必须是站点证书,后面跟中间证书,根证书不要放。
# 顺序绝不能反
cat your-domain.crt intermediate.crt > fullchain.pem
# 私钥要和证书配对
openssl rsa -in your-domain.key -check
# 改完必须测试再重载
nginx -t && nginx -s reload
然后原样再跑一次第四节那条 openssl s_client,确认 i: 变成两行。
还有两个地方要一起改,否则白改:
前面挂了 CDN 或反向代理的,证书要装在用户实际连的那一层。源站装好了、CDN 没装,用户看到的还是 CDN 的证书。
用宝塔面板的,SSL 页面那个「证书(PEM 格式)」框里粘完整的两段再保存,然后重启 Nginx。只粘站点证书那一段,面板不会报错,但链是断的——这是最容易骗过自己的一种情况。
六、一次真实的排查记录(照这个顺序走)
我自己的站也踩过一次。现象是:桌面 Chrome 正常,微信里打开提示不安全。
- 先确认不是自己这边的问题:换手机、关 WiFi 用 4G,复现了,排除本地缓存。
- 跑第四节那条命令,只输出一行
i:。问题确定:链不全。 - 去看 Let’s Encrypt 的目录,
fullchain.pem本身是好的,但 Nginx 的ssl_certificate指着cert.pem——之前手工改过一次,顺手改错了。 - 改成指向
fullchain.pem,nginx -t通过,reload。 - 再跑一次 s_client,两行
i:。微信里打开,绿锁回来了。
真正的修复只用了六分钟。花掉的时间全在「我这边明明是好的」这个错觉里绕圈。
补一个细节:验证时最好换一个没用过的浏览器,或者把浏览器完全关掉重开。证书的判定结果会被缓存,容易看到旧结论。
七、另一种麻烦:链是全的,但根不受信
这种情况 openssl s_client 能看到两行 i:,链完整,浏览器却还是报 AUTHORITY_INVALID。原因不在你,在对方系统的信任库里没有这张根证书。
最出名的例子就是 Let’s Encrypt。它的根 ISRG Root X1 直到 Android 7.1.2 才被加进安卓系统信任库。在那之前的安卓机器,靠的是 DST Root CA X3 的交叉签名,而那条交叉签名在 2021-09-30 到期,到期当天全球一大片老安卓设备突然打不开 HTTPS 站点。
所以判断顺序永远是:自己电脑正常 + 客户报不安全 + 服务器确实发了两张证书 → 对方系统太老,或者用的是企业自签根。
能选的路只有三条:
| 做法 | 代价 |
|---|---|
| 换一家兼容老系统更好的 CA(商业 DV 证书,链到 DigiCert / GlobalSign 的老根) | 要花钱,一年几十到几百 |
| 维持现状,明确告知不支持 Android 7.1.2 以下的设备 | 这类设备现在占比已经很低,多数场景可以接受 |
| 做双证书(RSA + ECDSA)覆盖不同客户端 | 配置变复杂,收益有限 |
对外营业的商业站值得为兼容性花这笔钱,个人博客不必。判断标准是你的客户里有没有人在用老机器——而不是你的技术洁癖。
八、五个长得像链问题的假故障
1)SNI 缺失。老客户端、某些监控探针、用 IP 直连的 curl,都不带 SNI。表现是「证书域名不对」,但其实链是好的。
2)服务器时间不准。虚拟机时钟漂移很常见。证书明明没过期,却报 not yet valid 或 expired。查一下 date、timedatectl,偏了就校时。
3)只写了 CN,没写 SAN。现代浏览器只看 SAN,CN 字段早就不看了。证书里没有 SAN 的域名,等于没绑这个域名。自己查:openssl x509 -in cert.pem -noout -text | grep -A1 "Subject Alternative Name"。
4)中间证书过期。前面提过,最隐蔽的一种。单独查中间证书的有效期:openssl x509 -in intermediate.crt -noout -dates。
5)客户端证书(双向 TLS)。服务端要求客户端也出示证书,浏览器没装,握手直接失败。这种往往不报「不安全」,而是「连接被重置」,很容易误判成网络问题。
九、发出去之前跑一遍这张清单
| # | 检查 | 通过标准 |
|---|---|---|
| 1 | openssl s_client 看链 |
i: 至少两行 |
| 2 | Verify return code |
0 (ok) |
| 3 | 站点证书和中间证书的有效期 | 两个都没过期,中间证书别快到期 |
| 4 | SAN 字段 | 裸域和 www 都在里面 |
| 5 | 手机开 4G(关 WiFi)打开一次 | 不报错 |
| 6 | 微信内置浏览器打开一次 | 不报错 |
| 7 | SSL Labs 评级 | A 或以上 |
| 8 | 到期时间 | 写进日历,提前一个月处理 |
第 5 和第 6 条最关键,也是所有人都会漏的两条。所有「我这边好的、客户不行」的争议,基本都卡在没做这两步。
十、这件事为什么值得较真
因为它直接掉转化。浏览器那个红色的「您的连接不是私密连接」,绝大多数人不会点「高级」继续,直接关掉。你的内容再好、工具再好用,他看不到。
因为它会让程序静默失败。支付回调、第三方 API、webhook、服务器之间的 curl,证书链不全时不一定报得那么清楚,有时候只是返回一个空字符串,然后你花半天查业务逻辑。
因为修它只要一条命令。比起排查一个 502,这件事性价比高得离谱:cat 拼一下、nginx -t、reload,完事。
顺便说一句:证书装好不等于永远没事。Let’s Encrypt 的证书三个月一换,自动续期偶尔会静默失败(首次连接超时、验证路径被拦),续期成功了但没 reload 也会继续用旧证书。所以清单里第 8 条那条日历提醒,比什么都管用。
相关:HTTP 和 HTTPS 的差别——如果还不清楚 TLS 到底加密了哪一部分,先看那一篇。










暂无评论内容