浏览器访问 HTTPS 站点,提示 您的连接不是私密连接 ERR_SSL_PROTOCOL_ERROR SSL_ERROR_HANDSHAKE_FAILURE,或者 curl 直接报 SSL certificate problem、sslv3 alert handshake failure——这些都是 TLS 握手失败的典型表现。握手是 HTTPS 建立连接的第一步,一旦失败,后面的 HTTP 通信根本无从谈起,整站直接不可用。
握手失败的原因集中在四类:证书链不完整、TLS 协议版本不匹配、加密套件不匹配、证书域名/有效期问题。它们报错信息相似、排查手段却很不一样,盲目重启或重签证书往往无效。这篇文章先简述 TLS 握手做了什么,再逐类讲解每种失败的原因和表现,重点用 openssl s_client 这个"瑞士军刀"教你怎么一步步确认到底是哪一类问题,最后给出 Nginx 的 TLS 配置建议和一套排查清单。读完你能自己判断"是证书问题还是协议/套件问题",并给出针对性修复。
先给一个判断方向:如果连服务器证书都拿不到(拿不到证书链),多半是证书链或证书本身的问题;如果证书能拿到但协商失败,多半是 TLS 版本或加密套件不匹配。
一、TLS 握手做了什么
HTTPS 在 HTTP 之下有一层 TLS,握手大致分几步:
客户端 ──ClientHello(支持的TLS版本、加密套件列表、随机数)──> 服务器
服务器 ──ServerHello(选定版本、套件)+ 证书链 + 密钥交换──> 客户端
客户端 ──验证证书 → 生成会话密钥 → 加密的 Finished──> 服务器
服务器 ──Finished──> 客户端
之后:应用数据(HTTP)在加密通道中传输
握手失败就发生在这些步骤中的某一步。常见失败点:
- 服务器发来的证书链不完整,客户端无法验证到受信任的根证书。
- 双方没有交集的 TLS 版本(比如客户端只支持 TLS 1.3,服务器只开 TLS 1.0)。
- 双方没有交集的加密套件(比如服务器只允许现代套件,客户端是老设备)。
- 证书域名不匹配或已过期。
二、握手失败的现象与报错对照
不同工具、不同原因报出来的错误措辞不同,先学会看。
| 现象/报错 | 可能原因 |
|---|---|
certificate has expired |
证书过期 |
certificate is not trusted / unable to get local issuer certificate |
证书链不完整或缺中间证书 |
hostname mismatch / doesn't match the certificate |
证书域名与访问域名不符 |
ERR_SSL_PROTOCOL_ERROR |
协议/端口问题,或握手协商失败 |
sslv3 alert handshake failure |
无共同加密套件或 TLS 版本不匹配 |
wrong version number |
用 HTTPS 连了 HTTP 端口(如 80) |
tlsv1 alert protocol version |
服务器拒绝客户端提议的 TLS 版本 |
no shared cipher |
服务器与客户端加密套件无交集 |
三、证书链问题(最常见)
这是生产环境握手失败的头号原因。TLS 证书的信任是链式的:服务器必须把"服务器证书 + 中间证书"一起发给客户端,客户端再用本地根证书验证整条链。
根证书(浏览器内置) → 中间证书 → 服务器证书(你的域名)
↑
很多服务器只装了这一环,缺中间证书 → 握手失败
典型场景:从 CA 下载了证书,只把域名证书配到 ssl_certificate,忘了把中间证书拼上去。桌面浏览器因为能自动下载中间证书(AIA 机制)可能正常,但手机 App、微信内置浏览器、部分 HTTP 客户端、老旧设备就会报"证书无效"——这是"电脑能开、手机打不开"的经典原因。
修复方式是把证书链按服务器证书在前、中间证书在后的顺序拼接:
# 拼接顺序:先域名证书,再中间证书(CA 提供的 ca-bundle 或 intermediate)
cat your_domain.crt intermediate.crt > fullchain.pem
# 检查拼接结果里包含几张证书
grep -c 'BEGIN CERTIFICATE' fullchain.pem
# 正常至少 2 张(域名证书 + 中间证书)
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem; # 含中间证书的链
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
}
验证链是否完整:
# -showcerts 打印服务器返回的全部证书
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts
四、TLS 版本不匹配
服务器和客户端支持的 TLS 版本必须有交集。近年来大量站点为安全起见关闭了 TLS 1.0/1.1,只留 1.2/1.3,导致老客户端(老版本 Android、老浏览器、部分内嵌 WebView、老 SDK)握手失败。
# 分别测试服务器对 TLS 1.0 / 1.1 / 1.2 / 1.3 的支持
openssl s_client -connect www.example.com:443 -tls1_2 /dev/null | grep 'Protocol'
openssl s_client -connect www.example.com:443 -tls1_3 /dev/null | grep 'Protocol'
# 若报 unsupported protocol / handshake failure,说明该版本不被接受
Nginx 中控制最低版本:
server {
listen 443 ssl;
# 只允许 TLS 1.2 和 1.3(推荐,安全性好)
ssl_protocols TLSv1.2 TLSv1.3;
# 若业务必须兼容很老的客户端,可临时加入 TLSv1.1(不推荐长期使用)
# ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
}
取舍:安全最佳实践是只开 TLS 1.2/1.3。如果确实有老客户端无法升级(如内置的旧硬件、老 App),需要评估风险后决定是否放行 TLS 1.1,并优先推动客户端升级。
五、加密套件不匹配
加密套件(cipher suite)是握手时协商的具体加密算法组合。如果服务器配置的套件列表与客户端支持的列表没有交集,握手就会以 no shared cipher 或 handshake failure 结束。
# 看服务器选用了哪个套件
openssl s_client -connect www.example.com:443 -servername www.example.com /dev/null | grep -i 'Cipher'
# 输出示例:Cipher : TLS_AES_128_GCM_SHA256 (TLS 1.3) 或 ECDHE-RSA-AES128-GCM-SHA256 (TLS 1.2)
Nginx 的套件配置(注意 TLS 1.3 的套件是独立的):
server {
listen 443 ssl;
ssl_protocols TLSv1.2 TLSv1.3;
# TLS 1.2 及以下使用的套件(按优先级排列,越前越优先)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# 由服务器决定套件优先级,而不是客户端
ssl_prefer_server_ciphers on;
# TLS 1.3 的套件单独配置(较新版本 Nginx 支持)
ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
}
常见坑:套件列表配得过严(比如只留 ECDSA 套件),而你的证书是 RSA 的,就会无交集失败。套件类型必须和证书类型(RSA/ECDSA)匹配。
六、证书域名不匹配与过期
这两类问题现象明确,排查也直接。
- 域名不匹配:证书的
CN或SAN里不含你访问的域名。比如证书签给www.example.com,你用api.example.com访问就失败。多域名要用含多个 SAN 的证书,或通配符证书(*.example.com,注意通配符只覆盖一级)。 - 已过期:证书超过有效期。用命令看有效期:
# 查看证书的生效/过期时间和包含的域名(SAN)
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName
# 输出:
# notBefore=...
# notAfter=... ← 对比当前时间
# subject=CN = www.example.com
# subjectAltName=DNS:www.example.com, DNS:example.com
提醒:很多握手失败是"忘了续期"。建议用证书自动续期(如 certbot 定时任务),并加有效期监控告警。
七、openssl 排查实操
openssl s_client 是排查 TLS 问题的核心工具,关键是一定要带 -servername(SNI),否则服务器可能返回默认站点的证书,导致误判。
# 1) 完整读取握手信息(证书链、协议版本、套件)
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts /dev/null \
| grep -E 'Protocol|Cipher|Verify return code'
# Verify return code: 0 (ok) ← 0 表示链验证通过;非 0 多半是链问题
# 3) 看证书有效期与 SAN
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName
# 4) 测试是否支持某 TLS 版本
openssl s_client -connect www.example.com:443 -tls1_2 &1 | grep -i 'protocol\|error'
# 5) 测验 HTTP 层是否正常(绕开浏览器)
curl -vI https://www.example.com/
# 6) 本地校验证书链(不联网)
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
Verify return code 是重点:0 (ok) 表示链完整可信;20(unable to get local issuer certificate)通常意味着服务器发的链里缺中间证书;10(certificate has expired)是过期;62(hostname mismatch)是域名不符。
八、Nginx TLS 配置建议
一份稳妥的现代配置参考:
| 配置项 | 建议值 | 说明 |
|---|---|---|
ssl_protocols |
TLSv1.2 TLSv1.3 |
关闭 1.0/1.1,兼顾安全与兼容 |
ssl_certificate |
含中间证书的 fullchain | 链必须完整 |
ssl_ciphers |
ECDHE 系列现代套件 | 前向保密、性能好 |
ssl_prefer_server_ciphers |
on |
服务器主导套件选择 |
ssl_session_cache |
shared:SSL:10m |
会话复用,减少握手开销 |
ssl_session_timeout |
10m |
会话缓存有效期 |
server {
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 会话票据(TLS1.2)与会话缓存协同
ssl_session_tickets off;
}
改完标准动作:
nginx -t && nginx -s reload
风险提示:修改 TLS 协议/套件会直接影响所有 HTTPS 连接。收紧版本或套件前,务必先用 openssl s_client 模拟确认目标客户端仍能连上,最好有灰度或回滚能力;改配置前备份 /etc/nginx,一旦出现大面积握手失败,立即还原并 reload 恢复。
常见问题
Q:电脑浏览器能正常打开 HTTPS,手机却提示证书无效,为什么?
经典症状,99% 是证书链不完整——服务器只发了域名证书,没带中间证书。桌面浏览器能通过 AIA 自动补链,手机 App、微信内置浏览器、部分客户端不会,就报错。把中间证书拼接进 ssl_certificate 指向的文件即可。
Q:openssl s_client 显示 Verify return code: 20,是什么意思?
"unable to get local issuer certificate",即验证不到颁发者,通常是缺少中间证书。把 CA 提供的中间证书与域名证书拼接成完整链再配置即可。
Q:所有客户端都握手失败,最可能是哪类问题?
如果"所有"客户端都失败,优先怀疑证书本身(过期、域名不符)或端口/协议配置(如 HTTPS 配到 80 端口)。如果只是"部分老客户端"失败,多半是 TLS 版本或套件过严。用 openssl s_client 分别测不同协议版本就能区分。
Q:只开 TLS 1.2/1.3 会不会影响老用户?
会有一定影响,老系统(很旧的 Android、老浏览器、老 SDK)可能不支持。安全上推荐只开 1.2/1.3;若业务确实有老客户端,需要评估后决定是否临时放行 1.1,并尽快推动升级。
Q:报 no shared cipher 怎么解决?
说明服务器与客户端的加密套件无交集。检查 ssl_ciphers 是否配得过严、套件类型是否与证书类型(RSA/ECDSA)匹配,必要时放宽套件列表。用 openssl ciphers 可查看某套件串实际包含哪些。
总结
TLS 握手失败集中在四类原因:证书链不完整、TLS 版本不匹配、加密套件不匹配、证书域名或有效期问题;判断的起点是看"证书能不能拿到、拿到了能不能验过"。
证书链不完整是生产环境头号原因,典型表现是"电脑能开、手机/App 打不开",修复办法是把域名证书和中间证书按顺序拼接成完整链(fullchain.pem)。
协议版本与加密套件不匹配多表现为"部分老客户端失败",安全实践是只开 TLS 1.2/1.3,但需评估老客户端兼容性;配套件时务必与证书类型(RSA/ECDSA)匹配。
排查主力工具是 openssl s_client,务必带 -servername(SNI),重点看 Verify return code(0 为正常)、协议、套件和证书有效期/SAN。
任何 TLS 配置变更都影响全站 HTTPS,改前备份、改后用 openssl/curl 验证目标客户端、保留回滚路径,并通过自动续期和有效期告警避免"过期型"握手失败。
企业QQ咨询




