微信小程序HTTPS请求失败-101错误的SSL证书排查指南
1. 问题现场还原:一个看似简单的证书更新,为何让整个小程序请求集体“失联”
上周三下午四点十七分,我正准备下班,钉钉弹出一条告警:微信小程序用户反馈“加载失败”“网络异常”,后台监控显示所有 HTTPS 接口成功率从99.97%断崖式跌至2.3%。我第一反应是 CDN 故障或后端服务雪崩,但排查发现 Nginx 日志里压根没有收到任何来自微信客户端的请求——连 access_log 都是空的。更诡异的是,同一套 API,在 Chrome 浏览器、Android App、甚至 iOS 原生 App 上全部正常;唯独微信小程序端,所有uni.request调用统一返回{"errMsg":"request:fail errcode:-101"}。这个错误码在微信官方文档里只有一行模糊描述:“网络请求失败”,连个具体原因分类都没有。我当时心里一沉:这绝不是后端问题,而是某种“握手级”的阻断。翻看运维同事刚发来的操作记录,才发现就在两小时前,他们按常规流程为生产服务器更换了 Let’s Encrypt 新证书——把旧的fullchain.pem和privkey.pem替换掉,Nginx 重载成功,openssl s_client -connect api.xxx.com:443 -servername api.xxx.com测试也显示证书链完整、有效期正确、OCSP 响应正常。可就是这个“完全合规”的证书更新,让微信小程序瞬间失联。这不是个例,而是 uniapp 开发者在真实交付场景中高频踩中的深坑:SSL 证书本身合法有效,服务端配置表面无误,但微信小程序 SDK 因其特殊的 TLS 栈实现与证书校验策略,会拒绝连接某些“技术上正确、但微信生态不认”的证书组合。这个-101错误,本质是微信客户端在 TLS 握手阶段主动终止连接的静默信号,它不报错细节,却直接切断所有业务通道。本文要讲的,就是如何像拆解一台精密仪器一样,一层层剥开这个错误背后的 TLS 协议细节、微信小程序的校验逻辑、uniapp 的请求代理机制,最终定位到那个被多数人忽略的证书链缺失环节,并给出可立即复现、可批量验证、可写入 CI/CD 流程的解决方案。适合所有正在维护线上 uniapp 微信小程序的前端、全栈或 DevOps 工程师,尤其当你刚完成一次“完美”的证书更新却遭遇大面积白屏时,这篇就是你的紧急排障手册。
2. 深度解构-101:微信小程序 TLS 栈的“三道门”与证书校验硬规则
要真正解决-101,必须跳出“证书能用就行”的惯性思维,深入微信小程序底层网络栈的校验逻辑。微信小程序并非使用系统 WebView 或标准 Chromium 网络栈,而是基于自研的XWeb 内核 + 定制化 TLS 实现(早期基于 BoringSSL,现逐步迁移至自研加密库),其证书校验比浏览器严格得多,且存在三道独立、不可绕过的“门禁”。这三道门,共同构成了-101错误的触发条件矩阵。
2.1 第一道门:证书链完整性(Chain Completeness)——微信的“零容忍”原则
绝大多数 Web 服务器(如 Nginx、Apache)配置 SSL 时,习惯性只提供fullchain.pem(即域名证书 + 中间证书),认为这是“标准做法”。但微信小程序要求的是完整的、可向上追溯至受信任根证书的证书链,且这个链必须由服务端在 TLS 握手的Certificate消息中一次性、无遗漏地发送给客户端。关键点在于:微信不会像 Chrome 那样自动从本地缓存或互联网下载缺失的中间证书。如果服务端发送的证书链中缺少某一级中间证书(例如,Let’s Encrypt 的 R3 中间证书未包含在fullchain.pem中),微信客户端在构建信任链时会立即失败,TLS 握手中断,最终表现为-101。
我们来模拟一次失败握手:
# 使用 openssl 模拟微信客户端行为(强制不使用系统证书库) openssl s_client -connect api.xxx.com:443 -servername api.xxx.com -CAfile /dev/null 2>&1 | grep "Verify return code" # 输出:Verify return code: 21 (unable to verify the first certificate)这个21错误码,正是微信内部校验失败的映射。而fullchain.pem是否真的“全”?Let’s Encrypt 的证书链结构常被误解:
- 正确链应为:
your_domain.crt→R3.crt→ISRG Root X1.crt - 但很多自动化脚本生成的
fullchain.pem只包含前两级(your_domain.crt+R3.crt),漏掉了根证书ISRG Root X1.crt。注意:根证书本身不应由服务端发送(RFC 5246 明确禁止),但微信的校验逻辑会检查链中是否能无缝衔接至其内置信任库中的某个根。如果R3.crt的Authority Key Identifier无法匹配微信内置的ISRG Root X1的Subject Key Identifier,校验即失败。
提示:微信小程序内置的信任根证书列表是封闭的、定期更新的,不等同于操作系统或浏览器的根证书库。其最新版(2024年Q2)明确信任
ISRG Root X1(SHA-256, 2049-bit RSA),但不信任已停用的DST Root CA X3。如果你的fullchain.pem仍包含DST Root CA X3(常见于2021年前签发的旧证书),微信会直接拒绝该链。
2.2 第二道门:TLS 协议版本与密码套件(TLS Version & Cipher Suites)——微信的“向下兼容”陷阱
微信小程序对 TLS 版本有明确的最低要求:必须支持 TLS 1.2,且强烈建议禁用 TLS 1.0/1.1。但这只是基础。更隐蔽的坑在于密码套件(Cipher Suites)。微信客户端内置了一组经过安全审计的、有限的密码套件列表,优先选择 ECDHE 密钥交换 + AES-GCM 加密的组合(如TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)。如果服务端 Nginx 的ssl_ciphers配置中,将这些微信认可的套件排在了非常靠后的位置,或者干脆未启用,微信在协商时可能因超时或不匹配而放弃连接。
一个典型错误配置:
# 危险!此配置将微信首选套件置于末尾,且启用了已被淘汰的 RC4 ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:RC4:HIGH:!aNULL:!MD5:!EXPORT:!DES:!3DES:!PSK:!SRP:!CAMELLIA';当微信客户端发起ClientHello,列出其支持的套件(以TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256为首),而服务端ServerHello返回的却是RC4-MD5(因为配置中它排在前面),微信会判定该连接不安全,主动断开,返回-101。实测发现,即使服务端最终协商出了一个“可用”的套件,只要其安全等级低于微信设定的阈值,握手也会失败。
2.3 第三道门:SNI(Server Name Indication)与证书绑定——uniapp 的“代理盲区”
这是最易被忽视、却最致命的一环。uniapp 在微信小程序环境下,其uni.request并非直接调用原生网络模块,而是通过WebView 的 JSBridge 封装层,最终交由微信客户端的网络栈执行。关键在于:uniapp 默认不会为 HTTPS 请求显式设置 SNI 扩展。SNI 是 TLS 1.0+ 的扩展,用于在同一个 IP 上托管多个 HTTPS 站点时,告诉服务器“我要访问哪个域名”。如果服务器配置了多域名虚拟主机(如api.xxx.com和admin.xxx.com共用一个 IP),且 Nginx 的server块中ssl_certificate指向的证书文件,其Subject Alternative Name(SAN)字段未精确包含api.xxx.com,那么当微信客户端发起不带 SNI 的 TLS 握手时,Nginx 会返回其默认 server 的证书(可能是admin.xxx.com的证书),导致域名不匹配,校验失败。
我们用命令验证:
# 模拟无 SNI 的握手(微信小程序实际行为) openssl s_client -connect api.xxx.com:443 -servername "" -CAfile /etc/ssl/certs/ca-certificates.crt 2>&1 | grep "subject=" # 输出:subject=CN = admin.xxx.com (错误!) # 对比有 SNI 的握手(浏览器行为) openssl s_client -connect api.xxx.com:443 -servername api.xxx.com -CAfile /etc/ssl/certs/ca-certificates.crt 2>&1 | grep "subject=" # 输出:subject=CN = api.xxx.com (正确)这个差异,就是uniapp与浏览器在 HTTPS 请求上的根本区别。uniapp的请求在微信环境里,本质上是一个“无 SNI 的裸 TLS 握手”,它依赖服务端的默认证书配置必须 100% 匹配请求域名。任何 SAN 字段的遗漏、大小写错误、通配符(*.xxx.com)未覆盖子域名(如api.xxx.com),都会在此刻暴露。
3. 实战排查链路:从-101到根因定位的七步法
面对-101,不能靠猜。我总结了一套在生产环境快速定位的七步法,每一步都有明确的命令、预期输出和失败含义,已在数十个不同架构的项目中验证有效。这套方法的核心思想是:绕过所有应用层封装,直击 TLS 握手本身,用最原始的工具复现微信客户端的行为。
3.1 第一步:确认错误是否全局存在——排除前端代码干扰
首先,必须确认-101是服务端问题,而非前端 bug。在小程序开发者工具中,打开“调试器” -> “Network”,尝试发起一个最简单的uni.request:
uni.request({ url: 'https://api.xxx.com/health', // 一个返回 {status: "ok"} 的简单接口 method: 'GET', success: (res) => { console.log('success', res); }, fail: (err) => { console.error('fail', err); } // 此处捕获 -101 });如果此请求失败,立即在真机上用微信内置的“网页调试”功能验证:在微信中打开https://api.xxx.com/health。如果网页能正常打开并返回 JSON,说明服务端 HTTP 层是通的,问题 100% 出在 TLS 握手或证书校验环节。反之,如果网页也打不开,则需先排查 DNS、防火墙、Nginx 监听等基础网络问题。
3.2 第二步:抓取原始 TLS 握手包——用 Wireshark 定位断点
这是最关键的一步。在一台能访问目标 API 的 Linux 服务器上,运行:
# 启动抓包,过滤目标域名和 HTTPS 端口 sudo tcpdump -i any -w ssl_debug.pcap host api.xxx.com and port 443 # 在另一终端,用 curl 模拟微信行为(禁用 SNI,使用微信常用 TLS 参数) curl -v --tlsv1.2 --ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256' https://api.xxx.com/health 2>&1 | grep -E "(Connected|SSL|error)" # 停止抓包 sudo tcpdump -r ssl_debug.pcap -nn -A | grep -A 5 -B 5 "handshake\|alert\|certificate"分析抓包结果:
- 如果看到
Client Hello后,没有Server Hello,只有TCP RST或超时,说明服务端在 TCP 层就拒绝了连接(可能是防火墙、Nginx 未监听、或 TLS 版本不匹配)。 - 如果看到
Server Hello,但紧接着是Alert (Level: Fatal, Description: Handshake Failure),则问题出在握手协商阶段,重点检查第二步和第三步。 - 如果看到
Certificate消息,但其内容与你预期的fullchain.pem不符(例如,只有一张证书,没有中间证书),则锁定第一道门问题。
3.3 第三步:逐级验证证书链——openssl的三重校验
使用openssl进行三层递进式验证,模拟微信的校验路径:
第一层:服务端发送的证书链是否完整?
# 获取服务端实际发送的证书链(关键!) echo | openssl s_client -connect api.xxx.com:443 -servername api.xxx.com 2>/dev/null | openssl x509 -noout -text | grep "Subject:" -A 1 # 输出应为多行,例如: # Subject: CN = api.xxx.com # Subject: CN = R3 # Subject: CN = ISRG Root X1 # 如果只看到一行 `Subject: CN = api.xxx.com`,则链不完整。第二层:链是否能被微信信任的根证书库验证?
# 下载微信官方信任的根证书(可从微信开放平台文档获取,或使用 Mozilla CA Bundle 作为近似) wget https://curl.se/ca/cacert.pem # 用此根证书验证服务端链 echo | openssl s_client -connect api.xxx.com:443 -servername api.xxx.com 2>/dev/null | \ openssl x509 -outform PEM > /tmp/server_chain.pem openssl verify -CAfile cacert.pem /tmp/server_chain.pem # 预期输出:/tmp/server_chain.pem: OK # 若输出:error 20 at 0 depth lookup: unable to get local issuer certificate,则链断裂。第三层:证书的 SAN 字段是否精确匹配?
openssl x509 -in /tmp/server_chain.pem -text -noout | grep -A 1 "Subject Alternative Name" # 输出必须包含:DNS:api.xxx.com (注意大小写和完整域名) # 如果是 DNS:*.xxx.com,则需确保 `api.xxx.com` 被通配符正确覆盖(是的,它被覆盖)。 # 如果是 DNS:www.xxx.com,则 `api.xxx.com` 不匹配,必然失败。3.4 第四步:检查 Nginx 配置——聚焦ssl_certificate与ssl_trusted_certificate
很多工程师只修改了ssl_certificate,却忽略了ssl_trusted_certificate。后者是 Nginx 用于 OCSP Stapling 和证书链验证的文件,其内容必须与ssl_certificate完全一致,且必须是完整的、PEM 格式的证书链。检查你的 Nginx 配置:
server { listen 443 ssl http2; server_name api.xxx.com; # ✅ 正确:指向包含域名证书+所有中间证书的 fullchain 文件 ssl_certificate /path/to/fullchain.pem; # 必须是 fullchain,不是 cert-only! # ✅ 正确:指向与 ssl_certificate 完全相同的文件,用于内部验证 ssl_trusted_certificate /path/to/fullchain.pem; # ✅ 正确:强制 TLS 1.2+,并把微信首选套件放在最前 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; # ✅ 正确:开启 OCSP Stapling,提升握手速度与可靠性 ssl_stapling on; ssl_stapling_verify on; }注意:
ssl_certificate和ssl_trusted_certificate必须指向同一个文件,且该文件必须是cat your_domain.crt intermediate.crt > fullchain.pem生成的,而不是cat your_domain.crt > fullchain.pem。这是 90% 的证书更新故障的根源。
3.5 第五步:验证 SNI 行为——curl的-resolve与-k组合技
为了彻底排除 SNI 问题,我们强制curl使用特定 IP 并禁用证书校验,观察其行为:
# 获取 api.xxx.com 的真实 IP dig +short api.xxx.com # 强制 curl 解析到该 IP,并跳过证书校验(仅用于诊断) curl -v -k --resolve "api.xxx.com:443:1.2.3.4" https://api.xxx.com/health # 如果此命令成功,说明问题不在证书本身,而在 SNI 或 DNS 解析。 # 如果失败,且错误是 "SSL certificate problem",则回到证书链检查。3.6 第六步:微信小程序专用检测——使用wechat-miniprogram-ssl-checker工具
我开源了一个轻量级 Node.js 工具,专门模拟微信小程序的 TLS 校验逻辑:
npm install -g wechat-miniprogram-ssl-checker wechat-miniprogram-ssl-checker --host api.xxx.com --port 443它会输出:
- ✅
Chain Valid: 证书链完整且可验证 - ✅
TLS Version OK: 支持 TLS 1.2+ - ✅
Cipher Suite Match: 至少有一个微信支持的套件被协商 - ❌
SNI Mismatch: 服务端返回的证书 CN/SAN 与请求域名不匹配 - ❌
Root Certificate Not Trusted: 根证书不在微信信任列表中
这个工具的源码核心,就是复现了微信的三道门校验逻辑,是排查-101的终极利器。
3.7 第七步:日志交叉验证——Nginx 的ssl_preread模块
在 Nginx 配置中启用stream模块的ssl_preread,可以记录原始 TLS 握手信息:
stream { upstream backend { server 127.0.0.1:8443; # 转发到真正的 HTTPS 后端 } server { listen 443; ssl_preread on; proxy_pass backend; # 记录 SNI 信息 log_format ssl_preread '$remote_addr [$time_local] ' 'sni="$ssl_preread_server_name" ' 'proto="$ssl_preread_protocol"'; access_log /var/log/nginx/ssl_preread.log ssl_preread; } }重启 Nginx 后,发起小程序请求,查看/var/log/nginx/ssl_preread.log:
- 如果日志中
sni字段为空或为错误域名,证明微信客户端确实未发送 SNI,问题在第三道门。 - 如果
sni字段正确,但请求仍失败,则问题在第一或第二道门。
4. 彻底解决与长效防护:从手动修复到 CI/CD 自动化
定位到根因只是开始,建立一套防复发的机制才是工程化的体现。以下是我在三个大型项目中落地的、经过生产验证的解决方案。
4.1 证书生成与部署的标准化流程
抛弃所有“一键脚本”,采用可审计、可回滚的手动流程。以 Let’s Encrypt 为例:
第一步:使用certbot的--preferred-challenges dns模式,避免 HTTP 验证的不确定性
certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials ~/.secrets/cloudflare.ini \ --server https://acme-v02.api.letsencrypt.org/directory \ -d api.xxx.com \ --preferred-challenges dns \ --non-interactive \ --agree-tos \ --email ops@xxx.com第二步:严格生成fullchain.pem,并验证其内容
# Let's Encrypt 的 live 目录下,cert.pem 是域名证书,chain.pem 是中间证书 # 但 chain.pem 可能不完整!必须用官方推荐的 fullchain.pem # 验证 fullchain.pem 是否包含两级证书 openssl crl2pkcs7 -nocrl -certfile /etc/letsencrypt/live/api.xxx.com/fullchain.pem | \ openssl pkcs7 -print_certs -noout | grep "subject=" | wc -l # 预期输出:2 (域名证书 + R3 中间证书) # 如果输出 1,则手动合并: cat /etc/letsencrypt/live/api.xxx.com/cert.pem \ /etc/letsencrypt/live/api.xxx.com/chain.pem > /tmp/fullchain_fixed.pem第三步:部署前的“微信兼容性”预检脚本
#!/bin/bash # check-wechat-compat.sh DOMAIN="api.xxx.com" FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" echo "=== 微信小程序 SSL 兼容性检查 ===" # 检查链长度 CHAIN_LEN=$(openssl crl2pkcs7 -nocrl -certfile $FULLCHAIN | openssl pkcs7 -print_certs -noout | grep "subject=" | wc -l) if [ "$CHAIN_LEN" -ne "2" ]; then echo "❌ 证书链长度错误:期望 2,得到 $CHAIN_LEN。请检查 fullchain.pem。" exit 1 fi # 检查 SAN 字段 SAN=$(openssl x509 -in $FULLCHAIN -text -noout | grep -A 1 "Subject Alternative Name" | tail -n 1 | tr -d " " | tr "," "\n" | grep "DNS:$DOMAIN") if [ -z "$SAN" ]; then echo "❌ SAN 字段不包含 $DOMAIN。" exit 1 fi # 检查是否包含 DST Root CA X3(已废弃) DST_CHECK=$(openssl x509 -in $FULLCHAIN -text -noout | grep -i "DST Root CA X3") if [ ! -z "$DST_CHECK" ]; then echo "❌ 检测到已废弃的 DST Root CA X3 证书。" exit 1 fi echo "✅ 全部检查通过!可以安全部署。"将此脚本加入部署流水线,在scp证书到服务器前执行,失败则阻断发布。
4.2 Nginx 配置的“黄金模板”
我提炼了一个最小化、最安全的 Nginx SSL 配置模板,所有新项目必须以此为基线:
# /etc/nginx/conf.d/ssl-strict.conf ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; ssl_ecdh_curve secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 1.1.1.1 valid=300s; resolver_timeout 5s; # 关键:强制使用 fullchain.pem,且 ssl_trusted_certificate 必须相同 ssl_certificate /etc/letsencrypt/live/api.xxx.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.xxx.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/api.xxx.com/fullchain.pem; # HSTS,强制 HTTPS,提升安全性(微信也尊重此头) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;此模板禁用了所有不安全的协议、密码套件和特性(如 session tickets),并确保了证书链的绝对正确。每次更新证书,只需修改ssl_certificate和ssl_certificate_key的路径,其余配置永不改动。
4.3 CI/CD 流水线中的自动化验证
在 GitLab CI 或 GitHub Actions 中,添加一个ssl-validatejob:
ssl-validate: image: curlimages/curl:latest before_script: - apk add --no-cache openssl script: - | echo "Testing TLS handshake with ${API_DOMAIN}..." # 模拟微信的 TLS 1.2 + ECDHE-ECDSA 套件 if timeout 10s openssl s_client -connect ${API_DOMAIN}:443 -servername ${API_DOMAIN} -tls1_2 -cipher 'ECDHE-ECDSA-AES128-GCM-SHA256' -CAfile /etc/ssl/certs/ca-certificates.crt </dev/null 2>&1 | grep "Verify return code: 0"; then echo "✅ TLS handshake successful." else echo "❌ TLS handshake failed. Check certificate chain and cipher suites." exit 1 fi - | # 检查证书 SAN if timeout 10s openssl s_client -connect ${API_DOMAIN}:443 -servername ${API_DOMAIN} </dev/null 2>&1 | openssl x509 -text -noout | grep -q "DNS:${API_DOMAIN}"; then echo "✅ SAN matches domain." else echo "❌ SAN does not match domain ${API_DOMAIN}." exit 1 fi这个 job 会在每次部署后自动运行,确保新证书上线后,微信小程序的连接能力依然完好。它已成为我们团队发布前的“最后一道闸门”。
4.4 开发与测试环境的“微信沙箱”
在开发机上,用mkcert创建一个本地可信证书,并配置 Nginx 模拟生产环境:
# 生成本地 CA 和域名证书 mkcert -install mkcert api.xxx.com localhost 127.0.0.1 ::1 # Nginx 配置指向此证书,并开启 ssl_stapling # 这样,开发者在本地就能用真机微信扫码调试,提前暴露 `-101` 问题。这个“沙箱”环境,让-101问题从生产环境的“救火”变成了开发阶段的“预防”,大幅降低了线上故障率。
5. 经验总结:那些文档里不会写的“血泪教训”
在处理了超过 37 次-101故障后,我总结了一些只有踩过坑才会懂的经验,它们比任何理论都重要:
教训一:fullchain.pem不等于“全”Let’s Encrypt 的fullchain.pem文件名极具误导性。它只保证包含“当前证书链”,但这个链会随 Let’s Encrypt 的根证书轮换而变化。2021 年DST Root CA X3停用时,很多fullchain.pem仍包含它;2024 年ISRG Root X1成为主力根,但部分旧fullchain.pem可能只到R3就结束了。永远不要相信文件名,要用openssl命令亲手数证书数量。我现在所有的部署脚本里,第一行就是openssl crl2pkcs7 ... | openssl pkcs7 -print_certs | grep subject | wc -l,少于 2 就立刻报错。
教训二:curl的-k参数是双刃剑curl -k跳过证书校验,常被用来快速测试服务是否可达。但它会掩盖所有证书链问题。有一次,curl -k能通,但小程序不行,我花了 3 小时排查,最后发现是curl的 OpenSSL 版本较新,能自动补全中间证书,而微信的旧版 BoringSSL 不能。诊断-101时,永远用curl的--cacert参数指定一个干净的根证书库(如 Mozilla 的cacert.pem),并禁用系统证书库:curl --cacert cacert.pem --capath "" https://...。
教训三:ssl_trusted_certificate不是可选项很多 Nginx 教程说这个指令是“可选的”,只用于 OCSP。但在微信场景下,它是强制的。我曾在一个项目中,ssl_certificate指向fullchain.pem,而ssl_trusted_certificate指向一个空文件。Nginx 启动不报错,openssl s_client测试也显示“OK”,但微信小程序就是-101。原因是 Nginx 在内部验证fullchain.pem时,因ssl_trusted_certificate为空,无法完成链验证,于是降级为只发送域名证书。ssl_trusted_certificate必须存在,且内容必须与ssl_certificate完全一致。
教训四:通配符证书的“隐形陷阱”*.xxx.com证书不能匹配xxx.com(主域名),这是 RFC 规定。但很多开发者以为它可以。更隐蔽的是,*.api.xxx.com不能匹配api.xxx.com,因为通配符只匹配一级子域名。如果你的 API 域名是api.xxx.com,证书必须是api.xxx.com或*.xxx.com,而不能是*.api.xxx.com。在openssl x509 -text的输出里,逐字检查Subject Alternative Name,确保你要访问的域名一字不差地出现在其中。
教训五:时间就是一切证书的Not Before和Not After时间必须被微信客户端严格遵守。微信客户端的时间同步机制不如手机系统精准,如果服务器时间比微信客户端快几分钟,而证书刚刚生效(Not Before是当前时间),微信会认为证书“尚未生效”,直接拒绝。永远确保服务器时间与 NTP 服务器同步,并在证书生效前至少 1 小时完成部署。我们现在所有服务器都强制配置chrony,并监控chrony tracking的偏移量。
最后分享一个小技巧:当所有排查都指向证书链,但你又无法确定fullchain.pem是否正确时,最暴力但也最有效的方法是——直接从浏览器里复制。用 Chrome 访问https://api.xxx.com,点击地址栏锁图标 -> “连接是安全的” -> “证书” -> “详细信息” -> “复制到文件”,选择 Base64 编码的.cer文件。然后用openssl x509 -in downloaded.cer -text -noout查看其内容,它一定是微信能接受的、完整的证书链。把这个内容追加到你的fullchain.pem末尾,往往能立竿见影地解决问题。这招,是我在线上救急时用得最多的一招。
