你刚把站点迁到 HTTPS。证书合法,地址栏短暂出现了锁图标——下一秒变成感叹号或三角形,写着 Not fully secure / Your connection to this site is not fully secure。打开 DevTools,能看到这种行:
Mixed Content: The page at 'https://yourdomain.com/' was loaded over HTTPS,
but requested an insecure element 'http://cdn.example.com/banner.jpg'.
This content should also be served over HTTPS.
更严格的情况下,资源会被直接屏蔽——hero 图、支付表单、地图组件压根不出来。这就是 mixed content(混合内容),现代浏览器把它当作安全倒退处理:Chrome 从 86 版起会自动升级被动资源,主动资源(脚本、iframe、fetch、XHR)直接拦掉。本文按”我属于哪种 mixed content”→ 永久修复 的思路来排查。
被动 vs 主动 mixed content(浏览器处理方式不同)
| 类型 | 例子 | Chrome 行为 |
|---|---|---|
| 被动 | <img>、<audio>、<video> 走 HTTP | 自动升级到 HTTPS;升级失败则回落 HTTP;控制台警告 |
| 主动 | <script>、<link rel="stylesheet">、<iframe>、fetch()、XMLHttpRequest、WebSocket | 直接拦截,元素根本不加载 |
所以你有时看到”Not secure”但页面没明显问题(被动),有时看到的”Not secure”伴随明显坏掉的功能(主动被拦)。
先判断属于哪一种
情况 1:自家模板里硬编码了 http://
老 WordPress 主题、手写 HTML、多年前复制粘贴的模板,里面 src= / href= / srcset= 是绝对的 http:// URL。
怎么看:
# 项目源码里
grep -rn 'src="http://' src/ public/ templates/
grep -rn 'href="http://' src/ public/ templates/
grep -rn '"http://' src/ | grep -v 'http://localhost'
浏览器里 DevTools → Network → 过滤 “http” → 找非 localhost 的 HTTP 请求。
修复:http:// 改成 https://。不确定目标支不支持 HTTPS,先 curl -I https://that-host.com/path 试一下。
情况 2:老 CMS / 数据库内容引用了 HTTP
WordPress / Ghost / 自研 CMS 里几千篇旧文章正文里硬编码了 <img src="http://yourdomain.com/wp-content/uploads/...">。模板没问题,数据库内容有问题。
怎么看:DevTools 里 mixed content 警告引用的是你自己的域名走 HTTP,类似 http://yourdomain.com/...,不是第三方。
修复(WordPress):
# 用 wp-cli,不要直接写 SQL UPDATE —— PHP 序列化数据会被破坏
wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' \
--all-tables --skip-columns=guid --dry-run
# 满意后去掉 --dry-run
其他 CMS:用它自己的迁移 / 替换工具。含序列化数据的表绝对不要直接跑 SQL 替换(会破坏长度前缀)。
情况 3:第三方 widget / 嵌入只有 HTTP 版
老牌统计脚本、社交分享按钮、聊天 widget、广告位 provider 没做 HTTPS 支持。要么没有 HTTPS endpoint,要么 HTTPS endpoint 证书有问题。
怎么看:DevTools 里 mixed content URL 是第三方域名。curl -I https://that-provider.com/widget.js 失败或报 SSL 错。
修复:(a) 联系服务方拿 HTTPS endpoint —— 多数都有,只是你用的代码片段太旧;(b) 把 widget 自托管:用自己的 HTTPS 反代;(c) 直接砍掉这个 widget。
情况 4:协议相对 URL(//cdn...)在开发环境跑到 HTTP
老代码用 //cdn.example.com/lib.js(不带协议),意思是”跟页面协议一致”。HTTPS 页就走 HTTPS,http://localhost 开发环境就走 HTTP——线上不出问题,开发出问题,常被错诊断。
怎么看:警告只在 localhost / 走 HTTP 的 staging 出现,线上没事。
修复:换成显式 https://。协议相对 URL 在 2014 年就被 W3C 弃用了,因为它鼓励 HTTP 回落路径。
情况 5:内联 JS / fetch / WebSocket 加载的资源
HTTPS 页里 fetch('http://api.example.com/data') 或 new WebSocket('ws://...')。主动 mixed content——会被拦。报错往往是 DevTools 控制台里一个 fetch 失败,不带 “mixed content” 字样。
怎么看:DevTools Network 里一个 fetch / XHR 状态写 (blocked:mixed-content)。从 HTTPS 页连 ws:// 的 WebSocket 会报 “An insecure WebSocket connection may not be initiated.”。
修复:JS 里 http:// 改 https://,ws:// 改 wss://。如果 API 不支持 HTTPS,客户端救不了——只能在自己服务器上 HTTPS 反代过去。
情况 6:<iframe> 来源是 HTTP
iframe 是主动内容,HTTPS 页里 <iframe src="http://..."> 直接被拦。
怎么看:iframe 区域空白,或浏览器自己显示错误。DevTools 控制台:Mixed Content ... iframe ... was blocked.
修复:换 HTTPS 源。一些老嵌入服务(旧地图、archive.org 2017 前的接口)只有 HTTP 嵌入——找新 endpoint 或自己托管一份。
最短修复路径
按命中率排序:
- DevTools → Console → 过滤 “Mixed Content” —— 拿到准确的违规 URL 清单。
- 分类:自家域名走 HTTP(CMS / 数据库)、自家模板(源码 find-replace)、第三方(按 provider 调研)。
- 自家域名 HTTP:跑一次数据库 search-replace。
- 模板硬编码:grep 源码、替换、提交、重新部署。
- 第三方:先查有没有 HTTPS endpoint;没有就砍或者反代。
- 作为兜底,在 HTTP 响应头加
Content-Security-Policy: upgrade-insecure-requests—— 浏览器会自动把所有子资源的http://重写成https://。被动资源静悄悄就好了;主动资源仍要求目标真的支持 HTTPS 才能跑起来。
# nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;
<!-- 设不了响应头时退而求其次 -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
- 验证:新开无痕窗口硬刷,DevTools 控制台应该是空的,地址栏锁图标是实心绿。
怎么确认修好了
# 扫一遍自己站点找 HTTP 引用
# 方式 A:用 Mozilla Observatory
curl -s "https://observatory.mozilla.org/api/v1/analyze?host=yourdomain.com" | jq
# 方式 B:命令行扫
curl -s https://yourdomain.com/ | grep -oE 'http://[^"]+' | sort -u
# 方式 C:专门的扫描器(推荐)
# https://www.whynopadlock.com/
# https://www.jitbit.com/sslcheck/
# 两者都会爬多页并按 URL 报告 mixed content
页面多的站点,唯一靠谱的长期保险是 CI 任务:grep -r 'http://' dist/ 命中就 fail build。
预防
- 所有新内容都用绝对
https://URL。 协议相对//已弃用,会让开发和生产环境表现不一致。 - 全局响应头里加
Content-Security-Policy: upgrade-insecure-requests—— 一行就能抵挡被动 mixed content 重新混进来。 - 加 CI 检查,扫构建后的 HTML 里非注释中的
http://,命中就 fail。 - WordPress / Ghost / CMS 站点:把规范站点 URL 一次性设到
https://,每次主题 / 插件更新后都验证一遍(有的主题会发硬编码 HTTP)。 - 每年审查一次第三方嵌入 —— 服务方偶尔会下线 HTTPS 或换 endpoint。过期的嵌入代码是最常见的回归来源。
FAQ
问:mixed content 会影响 Google 排名吗? 答:不直接,但间接会。HTTPS 自 2014 年起就是排名信号。一个明明走 HTTPS 却显示”Not secure”的页面会失去用户信任,跳出率和停留时间下降,反过来影响排名。Google 还会在 Chrome 地址栏屏蔽 mixed content 图片,影响转化。
问:upgrade-insecure-requests 能修所有问题吗?
答:能静默修复被动资源(图、音、视频)。也会尝试升级主动资源(脚本、iframe),但目标服务器不真支持 HTTPS 时,升级请求会失败,元素仍然加载不出来。所以每个目标都得支持 HTTPS——upgrade-insecure-requests 只是省了你手动改每条 URL。
问:我已经改了 WordPress 的站点 URL,怎么还提示 mixed content?
答:站点 URL 只影响新内容。老文章正文里硬编码着 http://。跑 wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' --skip-columns=guid 修数据库。--skip-columns=guid 不能省,省了会把老 feed 阅读器搞坏。
问:只有一个页面报 mixed content 警告,怎么回事? 答:那个页面引用了别的页面没有的东西——通常是某张横幅图、某个自定义嵌入或一段一次性的第三方脚本。打开那个具体页面的 DevTools,Network 看 HTTP 请求。
问:我用的第三方 widget 只有 HTTP 版,对方拒绝加 HTTPS。怎么办?
答:自己服务器上反代。在 https://yourdomain.com/widget-proxy/ 加一条路由,服务端拉那个 HTTP 资源,用你的 HTTPS 发出去。多一点延迟和带宽,但短期内除了砍掉 widget 这是唯一办法。