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-content | 0 条拦截 |
| HSTS 已开 | curl -I https://域名 | 含 Strict-Transport-Security |
| 证书有效期 | openssl x509 -enddate | 未过期 |
最稳的做法是把上面这几步固化进 CI:每次构建后跑一次混合内容扫描、每次部署前用 curl 验一次证书链,任一不通过就阻断发布。这样“人肉忘了改”不再成为故障来源,HTTPS 健康度也能持续可观测。
九、结语
HTTPS 不是“装上证书就完事”。混合内容让加密形同虚设,证书链缺口让老旧客户端集体报错。把 openssl、curl、自动化替换三件套固化进发布流程,再配合 certbot / cert-manager 自动续期(见 certbot 实战 与 cert-manager 实战),才能长治久安。




