HTTPS 混合内容与证书链故障排查实战

HTTPS 上线后浏览器仍提示“不安全”?混合内容(Mixed Content)证书链不完整,是两类最容易被忽视的 HTTPS 故障。本文用真实排错命令,从浏览器警告一路查到证书链缺口,给出可落地的根治方案。

一、什么是混合内容(Mixed Content)

当你的页面通过 HTTPS 加载,但其中的子资源(图片、脚本、样式表、iframe、字体、XHR 请求)却用 HTTP 加载时,就产生了混合内容。浏览器会认为“这条加密连接里混进了明文”,于是按风险等级区别对待:

  • 主动混合内容:HTTP 的脚本、iframe、CSS——可被中间人篡改页面行为,现代浏览器默认直接拦截。
  • 被动混合内容:HTTP 的图片、音视频——风险较低,浏览器通常只给警告,但小锁图标会变灰。

别小看这件事:混合内容不只是“丑陋的警告”。浏览器对主动混合内容的拦截会直接破坏功能;而对被动混合内容的降级,会让用户潜意识里觉得站点“不正规”。在搜索视角下,HTTPS 完整性是排名信号之一,长期挂着不安全标记对 SEO 也是负分。所以它是既要修、又值得一次性修干净的问题。

二、混合内容的三种典型表现

1. 地址栏小锁变灰或出现警告三角

页面本身没挂,但部分资源是明文加载,浏览器判定连接“不完全可信”。

2. 关键 JS/CSS 被拦截,页面崩了

如果被人拦截的是核心脚本(比如 jQuery、框架运行时),页面会直接白屏或功能瘫痪。

3. 控制台报 Mixed Content 错误

Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure script 'http://cdn.example.com/app.js'.
This request has been blocked; the content must be served over HTTPS.

三、证书链不完整:另一个沉默的杀手

另一种“HTTPS 不安全”的元凶是证书链不完整。浏览器报错 NET::ERR_CERT_AUTHORITY_INVALID 或提示证书链缺失,但你在服务器上用某些工具检查却显示正常——原因在于中间证书(Intermediate CA)没有随站点证书一起下发给客户端

常见诱因有三类:宝塔/Apache 只部署了叶子证书(leaf cert);Nginx 的 ssl_certificate 指向了单证书文件而非 fullchain;CDN 回源时漏传了中间证书。它和证书过期导致全站中断不同:过期是“整站全挂”,而证书链缺口往往是“部分老旧客户端、部分手机浏览器集体报错”,更隐蔽。

还有一个容易忽略的细节:证书文件的顺序。fullchain 必须是“叶子证书在上、中间证书在下、根 CA 在最下”的顺序,顺序反了部分严格客户端同样不认。用 openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout 可以打印出链里每一层的 subject/issuer,一眼确认中间证书确实在列。

四、排查工具箱:从浏览器到命令行

第一步,用 OpenSSL 看清证书链到底下发了几层:

openssl s_client -connect example.com:443 -servername example.com 2>/dev/null   | openssl x509 -noout -issuer -subject
# 期望看到:subject=你的域名证书,issuer=中间 CA;再用 -showcerts 看是否包含中间证书

第二步,用 curl 模拟“严格客户端”的校验行为(curl 默认会校验完整链):

curl -vI https://example.com 2>&1 | grep -i 'SSL certificate\|subject\|issuer\|verify'
# 若输出 SSL certificate problem: unable to get local issuer certificate,即证书链缺失

第三步,定位混合内容:在浏览器 DevTools 的 Network 面板筛选 mixed-content,或直接看 Console 里被拦截的 http:// 请求。把可疑域名记下来,进入下一步批量替换。

五、根治混合内容:自动化替换脚本

人工改一个个链接很容易漏。下面这段 Python 把页面/模板里的 http:// 资源统一改写为协议相对写法 //,既保留 HTTPS 又兼容历史链接:

import re, pathlib

def fix_mixed_content(html: str) -> str:
    # 把显式 http:// 第三方域名改写为协议相对 //
    return re.sub(r'http://([\w.-]+\.(?:js|css|png|jpg|jpeg|gif|svg|woff2?))',
                  r'//', html)

for f in pathlib.Path('templates').rglob('*.html'):
    txt = f.read_text(encoding='utf-8')
    new = fix_mixed_content(txt)
    if new != txt:
        f.write_text(new, encoding='utf-8')
        print('patched', f)
资源类型风险推荐方案
图片/字体低(被动)改为 https:// 或协议相对 //
JS/CSS高(主动)必须 https://,禁止明文
第三方域名确认对方支持 HTTPS,否则换源或代理
内联硬编码用模板变量统一拼接协议

五(续)、构建产物里的混合内容:Webpack / Vite 怎么治

现代前端大多由构建工具产出静态资源,硬编码的绝对 http:// 往往来自第三方 SDK 或历史配置。与其事后正则替换,不如在构建期就统一协议:

// vite.config.ts:用协议相对 base,产物里的资源引用自动变成 //
export default defineConfig({
  base: '//',            // 或显式 'https://your-cdn.com/'
  build: { assetsDir: 'assets' }
})

// webpack:output.publicPath 设为 '//cdn.example.com/' 即可避免硬编码 http

构建期固定之后,再配合第五步的自动化脚本兜底扫描,基本可以做到“新代码零新增混合内容”。

六、补全证书链的正确姿势

如果确认是链缺失,把中间证书拼接到站点证书之后:

cat example.com.crt intermediate.crt root.crt > fullchain.crt
# Nginx 必须指向 fullchain,而不是单证书:
# ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

这里有个高频坑:Let’s Encrypt 免费 SSL 证书自动续期默认生成 fullchain.pem(已含中间证书),但很多教程让你引用 cert.pem——那是单证书,缺中间链。K8s 场景则交给 cert-manager 自动注入完整链,配合 NGINX Ingress 的 TLS 配置基本不会踩这个坑。

如果你用了 CDN(如 Cloudflare、阿里云 CDN),还要确认“回源证书”同样完整:CDN 到源站这段如果是 HTTPS,源站下发的也必须带中间证书,否则 CDN 节点会报 526/525 一类回源 TLS 错误。很多“用户访问正常、CDN 后台却持续报错”的怪象,根子就在这。

七、Nginx 与反向代理里的坑

server {
  listen 443 ssl;
  server_name example.com;
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  add_header Strict-Transport-Security "max-age=63072000" always;

  location / {
    proxy_pass http://backend;            # 回源用 http 是正常且安全的
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

重点澄清一个误区:Nginx 反向代理proxy_pass http://backend 是服务器内部回源,走的是内网,本就不需要 HTTPS;真正出问题的,是“最终返回给浏览器的页面 HTML 里出现了 http:// 子资源”。所以修混合内容要改前端输出,而不是把回源改成 https。

八、上线前检查清单

检查项命令 / 方法预期结果
证书链完整openssl s_client -connect :443显示完整 issuer 链
无混合内容DevTools 过滤 mixed-content0 条拦截
HSTS 已开curl -I https://域名含 Strict-Transport-Security
证书有效期openssl x509 -enddate未过期

最稳的做法是把上面这几步固化进 CI:每次构建后跑一次混合内容扫描、每次部署前用 curl 验一次证书链,任一不通过就阻断发布。这样“人肉忘了改”不再成为故障来源,HTTPS 健康度也能持续可观测。

九、结语

HTTPS 不是“装上证书就完事”。混合内容让加密形同虚设,证书链缺口让老旧客户端集体报错。把 openssl、curl、自动化替换三件套固化进发布流程,再配合 certbot / cert-manager 自动续期(见 certbot 实战cert-manager 实战),才能长治久安。

上一篇 Git 工作流进阶实战:rebase 与提交规范
下一篇 just 命令运行器实战:替代 Makefile 的现代任务编排