修复 SSL Mixed Content 警告(HTTPS 站点显示 "Not Secure")

站点已是 HTTPS,浏览器却显示 "Not fully secure"。判断你属于 6 种 mixed content 的哪一种并彻底修好——含 2026 年最新 Chrome 行为。

一句话先说: 打开 DevTools 控制台,过滤 Mixed Content,看清它列出的那些 http:// URL。指向你自己域名,说明 CMS / 数据库里有旧内容——跑一次数据库 search-replace。指向第三方,就把代码片段换成它的 HTTPS endpoint,或者直接删掉。对被动资源(图片 / 音频 / 视频)想快速兜底,给响应头加一行 Content-Security-Policy: upgrade-insecure-requests。主动资源(脚本、iframe、fetch、WebSocket)仍然需要一个真正能用的 HTTPS 源——这些没有捷径。

你刚把站点迁到 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 was loaded over an insecure connection. This request has been
automatically upgraded to HTTPS.

或者,当升级无法完成时,你看到的是被屏蔽:

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

这就是 mixed content(混合内容):HTTPS 页面里掺进了走纯 HTTP 的子资源。现代浏览器把它当作安全倒退,会自动处理——不再悄悄把不安全的资源显示出来。顺带说个背景:Chrome 正在收尾它的 HTTPS-by-default 计划。截至 2026 年 6 月,“Always Use Secure Connections” 已对 Enhanced Safe Browsing 用户默认开启(Chrome 147,2026 年 4 月),并将在 Chrome 154(2026 年 10 月)对所有人成为默认,详见 Google 的 HTTPS by default 公告。清除每一个 http:// 子资源的压力,往后只会越来越大。

被动 vs 主动 mixed content(浏览器处理方式不同)

最重要的一个区分。它决定一个资源是静默升级还是直接坏掉。

类型例子当前 Chrome 行为(截至 2026 年 6 月)
被动(可升级)<img><audio><video><source>、CSS background-image 走 HTTP自动升级到 HTTPS。HTTPS 版能加载就没事。没有 HTTPS 版的话,资源不加载——不会回落到 HTTP。控制台会记录升级成功或失败
主动(可拦截)<script><link rel="stylesheet"><iframe>fetch()XMLHttpRequest、Web 字体、WebSocket直接拦截,元素根本不加载
任何指向 IP 地址的 HTTP 请求http://93.184.215.14/x.png直接拦截,不升级——连图片也一样。请用主机名

跟你可能读过的旧说法相比,有两点变了。第一,被动资源升级失败后浏览器不再回落到 HTTP——按 MDN 的 Mixed content 文档,请求被升级,HTTPS 不可用时就直接失败。第二,老的 block-all-mixed-content CSP 指令已弃用,现在该用的是 upgrade-insecure-requests

所以你有时看到”Not secure”并伴随明显坏掉的功能(主动被拦,或某张被动图片没有 HTTPS 版),有时只是锁图标降级成感叹号、页面看着没事。

先判断属于哪一种

情况 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 或自己托管一份。

最短修复路径

按命中率排序:

  1. DevTools → Console → 过滤 “Mixed Content” —— 拿到准确的违规 URL 清单。
  2. 分类:自家域名走 HTTP(CMS / 数据库)、自家模板(源码 find-replace)、第三方(按 provider 调研)。
  3. 自家域名 HTTP:跑一次数据库 search-replace。
  4. 模板硬编码:grep 源码、替换、提交、重新部署。
  5. 第三方:先查有没有 HTTPS endpoint;没有就砍或者反代。
  6. 作为兜底,在 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">
  1. 验证:用下一节的几项检查。

怎么确认修好了

别只信一个页面。mixed content 往往就藏在那篇没人点开的旧文章里。

# 方式 A:命令行扫单个页面
curl -s https://yourdomain.com/ | grep -oE 'http://[^"'"'"' ]+' | sort -u

# 方式 B:专门的爬虫(推荐——会检查多个页面)
# https://www.jitbit.com/sslcheck/   递归爬站,列出不安全的脚本 / 图片
# https://www.whynopadlock.com/       单页深度检查

想要可规模化、持续的报告,用 report-only(只报告) 模式跑一条 Content-Security-Policy,让浏览器把违规上报给你而不打断任何东西:

# 把每一次 mixed-content(及其他)违规记到你的 endpoint;不拦截任何东西
add_header Content-Security-Policy-Report-Only
  "upgrade-insecure-requests; report-to csp-endpoint" always;

最后手动确认一次:开无痕窗口、带着 DevTools 硬刷页面。控制台过滤 Mixed Content 应该是空的,地址栏锁图标应是实心(没有感叹号 / 三角形)。页面多的站点,唯一靠谱的长期保险是 CI 任务:grep -rE 'http://[^"]' dist/(排除注释和 http://localhost)命中就 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”的页面会失去用户信任,随之而来的跳出率、停留时间等信号变差,反过来影响排名。而且没有 HTTPS 版的被动图片现在干脆不渲染——那张坏掉的 hero 图或产品图会直接拖累转化。

问:upgrade-insecure-requests 能修所有问题吗? 答:它让浏览器在发请求前把每个 http:// 子资源重写成 https://,被动和主动资源都管。前提是:升级只有在目标确实用 HTTPS 提供该资源时才有效。目标不支持 HTTPS,升级请求就会失败,元素仍然加载不出来。所以每个目标主机都得支持 HTTPS——upgrade-insecure-requests 只是省了你手动改每条 URL,它没法让一个只有 HTTP 的主机凭空能用。

问:Chrome 154 / “Always Use Secure Connections” 现在默认开了,对我的 mixed content 有影响吗? 答:那个设置管的是顶层导航(直接输入或点击裸 http://yourdomain.com 时,加载前会弹警告)。mixed content 说的是已经走 HTTPS 的页面里的子资源——这是另一套机制,已经强制执行很多年了。按本文修好你的 http:// 子资源,才是清除页面内”Not secure”状态的办法,跟那个导航设置开不开无关。

问:我已经改了 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 这是唯一办法。

相关阅读

标签: #域名 #DNS #SSL #排查 #SSL 混合内容