网站HTTPS握手失败怎么办?TLS版本、证书链与加密套件排查

浏览器访问 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 验证目标客户端、保留回滚路径,并通过自动续期和有效期告警避免"过期型"握手失败。