1. 白屏现场一个http地址在https页面里被静默干掉先还原一个我最近被反复问到的场景客户有一套内部运营后台主域名已经切到 https但某个老旧的监控系统还跑在内网 http 上。为了不让人来回切标签页大家习惯把监控页面直接嵌进后台iframe srchttp://10.0.0.8:8080/monitor width100% height800/iframe结果打开后台主界面完全正常唯独预留的 iframe 区域一片白。很多人第一反应是后端服务挂了、端口不通、防火墙拦了、iframe 路径写错了。但我自己在现场检查后发现后端服务明明 200 响应单独用 http 协议直接访问也完全正常甚至把这个 iframe 放到一个 http 页面里都能正常显示放到 https 页面里就白屏。打开 DevTools 的 Console真正的原因才浮出来Mixed Content: The page at https://ops.example.com/dashboard was loaded over HTTPS, but requested an insecure resource http://10.0.0.8:8080/monitor. This request has been blocked; the content must be served over HTTPS.这就是标题里说的 mixed content 问题。类似的情况我见过太多次很多人会把锅甩给跨域然后去配 CORS、加代理折腾半天发现还是白屏也有人在网上找到一堆浏览器加启动参数、关安全策略的邪路方案当时能用换台机器又废了。这个案例里同时牵扯两个问题一个是混合内容mixed content导致的加载被拦截另一个是 iframe 与父页面之间的跨域通信。两个问题经常被混在一起讨论但本质完全不同解决方案也不同。这篇文章我不打算只给结论而是把浏览器的拦截机制、iframe 加载 http 的底层原因、生产环境下真正可落地的几种方案、以及嵌入成功后的跨域通信全套讲一遍最后附上我实际排查时的套路和踩坑记录。访问方式页面协议iframe 目标结果直接访问 iframe 地址httphttp://10.0.0.8:8080/monitor正常渲染普通 http 页面嵌入httphttp://10.0.0.8:8080/monitor正常渲染https 页面嵌入httpshttp://10.0.0.8:8080/monitor被拦截白屏2. mixed content拦截机制拆解浏览器到底在防什么2.1 主动混合内容与被动混合内容的区别浏览器把混合内容分成两类拦截策略完全不同。一类叫主动混合内容active mixed content包括 iframe、script、fetch/XHR、CSS、WebSocket 这类有能力修改页面逻辑、读取页面数据的资源另一类叫被动混合内容passive mixed content主要是图片、音频、视频这些相对静态的媒体资源。对于主动混合内容现代浏览器基本是零容忍。因为一个 https 页面本身经过完整加密如果允许向 http 地址加载 JavaScript 或 iframe那么中间人可以在传输过程中篡改脚本内容往页面里注入恶意代码那 https 的存在意义就完全被架空了。被动混合内容在历史上宽松过一阵子只显示不安全警告但不拦截。后来 Chrome 做了自动升级机制遇到 http 图片时先尝试用 https 版本替代目标站点如果支持 https 就能正常加载不支持就会加载失败。所以现在很多人的感受是图片偶尔裂掉这其实是自动升级失败后的表现。2.2 浏览器为什么只防https页面里的http请求理解混合内容先要理解浏览器的安全上下文体系。https 页面里发起的任何子资源请求都以当前页面必须是可信的、加密的为前提。如果允许这个页面去加载 http 资源等于在加密的信道里开了一个明文的口子中间人可以篡改的内容就不仅仅是那个子资源本身甚至可以通过 JavaScript 操控整个父页面。iframe 在这里尤其特殊它不是一个普通的资源而是一个完整的浏览上下文。iframe 被拦截本质上是因为浏览器把它当作可执行的文档而不是静态内容。你在 iframe 里加载一个 http 页面那个页面里可以跑自己的脚本、发自己的请求、读写自己的 DOM它具备完全等同于父页面的能力边界所以浏览器对 iframe 的混合内容检查严格得多。我用一个类比来解释https 页面就像一个带门禁的园区所有进入园区的快递都要走安检https 加密。现在你想让一个没有安检凭证明文 http、而且里面可能装着任意物品的集装箱直接开到核心办公区iframe门卫当然不会放行。要放行要么这个集装箱本身补办凭证目标站换 https要么你在园区门口安排一个安检点把拆出来的东西重新包装成合规包裹再送进去反向代理。2.3 版本演进从警告到直接拦截Chrome 对主动混合内容的策略是一步步收紧的。早年浏览器只是地址栏提示不安全页面照样加载到了 Chrome 80 前后主动混合内容开始默认拦截再往后的版本里用户甚至很难在地址栏里找到允许不安全内容的临时开关了。Firefox、Safari、Edge 也都在跟随类似的策略。这也意味着一个很残酷的事实纯前端没有任何配置项能真正绕过混合内容拦截。你看到的所谓在链接上右键、选择允许加载不安全脚本之类的操作只对当前这一台电脑、当前这一个页面、当前这一次访问有效刷新或者换用户就失效而且它还降低了整台机器的安全性。真正靠谱的路径只有一个让 iframe 的 src 地址变成 https 可访问的地址。3. iframe加载http的特殊性不报错、难发现、跨域叠加3.1 iframe是独立的浏览上下文协议的锅甩给子资源iframe 和普通图片、脚本最大的区别在于它创建了一个全新的浏览上下文。这个上下文有自己的 window、document、history甚至可以进一步加载自己的子资源。也就是说一个 http 的 iframe 页面里如果又引用了 http 图片、http 接口那这些子资源同样会因为混合内容被拦截。实际项目里我遇到过一种特别隐蔽的情况iframe 目标页面主文档已经被代理成了 https但页面内部有一个http://...的 ajax 请求结果整个页面加载出来一半数据区域全是空的。这种问题最难定位因为控制台里混合内容报错的对象不是 iframe 本身而是它内部的某个接口。所以排查的时候不能只看 iframe 的 src还要进目标页面里看它自身有没有继续引用 http 子资源。3.2 跨域与mixed content是两个维度的问题很多人在搜索这个问题时会把跨域和混合内容混在一起。我强调一下这是两个独立维度跨域指的是页面与 iframe 之间的协议、域名、端口不一致导致 JS 无法直接访问彼此的 DOM。这是浏览器同源策略管的事。混合内容指的是 https 页面加载 http 子资源被安全策略拦截。这是浏览器混合内容策略管的事。一个 https 页面加载https://third-party.example.com的 iframe跨域问题存在但不会有混合内容问题一个 https 页面加载http://10.0.0.8:8080的 iframe如果源站 http混合内容先拦住你跨域问题还没轮到出场。所以处理顺序一定是先解决可加载性混合内容再解决可交互性跨域通信。3.3 即使加载成功还有X-Frame-Options、CSP frame-ancestors、滚动条这些坑就算你把 iframe 的 src 换成了可访问的 https 地址距离真正能用还差几步。第一个常见的坑是目标站点设置X-Frame-Options: DENY或X-Frame-Options: SAMEORIGIN。只要响应头里出现这个字段浏览器同样会在控制台报Refused to connect拒绝页面被嵌入。现在的安全规范更推荐用 CSP 的frame-ancestors指令控制但这个指令和X-Frame-Options同时存在时两者都会生效任何一个拒绝都不行。像 dataease 的社区版就明确禁止通过 iframe 嵌入很多商业系统为了防止点击劫持也都默认不让你嵌这不是技术上做不到而是对方从安全策略上主动拒绝。第二个小坑是 iframe 样式。默认 iframe 自带边框很多项目还要额外处理隐藏滚动条。传统做法是在标签上写scrollingno但 HTML5 里这个属性已经废弃我通常用 CSSiframe { border: 0; overflow: hidden; width: 100%; height: 100%; }注意overflow: hidden只是让 iframe 自身不出现滚动条如果内部页面内容超出高度真正的内容会被裁切掉这牵扯到高度自适应后面我会专门讲。4. 生产环境最稳方案用Nginx反向代理把http洗成同源https4.1 原理把混合内容变成纯https的同源请求最常用的解法也是我上手第一个推荐的方案在你自己可控的 https 域名下面开一个反向代理路径把请求转发到后端的 http 服务。浏览器最终看到的 iframe 地址是https://ops.example.com/ext/monitor/它是一个 https 地址而且和父页面同源。浏览器在安全层面完全满意加密、同源、没有混合内容、没有跨域。真正的 http 请求发生在服务器到服务器之间也就是 Nginx 到10.0.0.8:8080这个过程不经过浏览器不受 mixed content 策略约束。这个方案最大的好处是解耦前端代码几乎不用改只需要改 iframe 的 src 地址目标 http 服务也完全不用动老系统继续跑在它习惯的环境里。你在中间加的一层是一个协议翻译官。4.2 Nginx配置示例与路径映射规则下面这份配置是我在实际项目里反复用过的模板直接用即可server { listen 443 ssl; server_name ops.example.com; # SSL证书配置省略 location /ext/monitor/ { proxy_pass http://10.0.0.8:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_redirect http://10.0.0.8:8080/ /ext/monitor/; } }这里最容易被问到的是proxy_pass后面到底要不要带斜杠。规则很简单location /ext/monitor/后面如果带斜杠比如proxy_pass http://10.0.0.8:8080/;那么请求/ext/monitor/foo会被转发到http://10.0.0.8:8080/foo也就是 location 匹配到的前缀会被替换成代理目标里的路径。如果proxy_pass后面不带斜杠proxy_pass http://10.0.0.8:8080;那么请求会被原样转发为http://10.0.0.8:8080/ext/monitor/foo。我建议在大多数场景下用带斜杠的写法这样目标服务不需要感知自己被套了一层路径所有相对路径资源都能正常工作。4.3 最容易翻车的几个点重定向、cookie、WebSocket、源站绝对地址第一重定向问题。目标站如果做了 302 跳转Nginx 默认会把后端响应的Location头直接透传给浏览器。如果 Location 里写的是http://10.0.0.8:8080/...浏览器一看又是 http立刻拦掉。所以我上面的配置里特意加了proxy_redirect http://10.0.0.8:8080/ /ext/monitor/;把重定向目标改写成可以被安全加载的地址。第二Cookie 问题。如果目标站登录态依赖 Cookie且 Cookie 设置了Secure属性浏览器在 https 环境下会把它发给代理域名但代理转发给后端时后端看到的主机名和协议是通过Host、X-Forwarded-Proto传过去的有些应用会据此生成错误的回跳地址。另外注意后端通过Set-Cookie设置的 Cookie 如果没有Secure浏览器在 https 页面接受与否取决于具体策略和 SameSite 属性通常没问题但建议后端显式设置Secure并确认 Cookie 的作用域与代理域名匹配。第三WebSocket 问题。如果目标 iframe 页面内部用 WebSocket 和服务端通信Nginx 默认不认 WebSocket 协议升级需要显式设置Upgrade和Connection头就是我上面写的proxy_http_version 1.1那三行。第四也是最麻烦的目标页面内部存在指向 http IP 的绝对路径资源。比如它的前端代码里写了http://10.0.0.8:8080/api/getData这种请求发生在 iframe 加载完成后浏览器依然会按混合内容拦截。解决办法通常只有两个要么目标站改代码把请求改成相对路径要么你写sub_filter对响应体做内容替换。后者我强烈建议慎用因为 HTML 里炸出意想不到的格式时正则替换很容易误伤。我在一个项目里看到同事用sub_filter http://10.0.0.8:8080 /ext/monitor替换结果把页面里展示给用户的一段说明文字里的链接也替换了用户点了那个说明链接跳到了一个空路径上。这种坑排查起来非常费时间。5. 目标服务直接升级https的适用场景与坑5.1 升级https后iframe的使用方式如果被嵌入的服务是你自己团队维护的那最干净利落的方案是让它直接支持 https。改造之后 iframe 的 src 直接写成https://target.example.com/...不再有混合内容问题反向代理也省了。升级方式取决于部署形态。传统虚拟机直接部署的可以在服务前面套一个 Nginx 做 TLS 终止跑在 Kubernetes 里的可以用 Ingress 或者云厂商的负载均衡器配置证书。无论哪种本质上都是把 TLS 终结在 L7 层后端服务本身仍然跑 http和前面反向代理方案的区别在于这是一个独立的域名而非某个路径。但这种方案只解决了能不能加载的问题没有解决跨域问题。iframe 的域名和目标站域名如果不同你依然需要 postMessage 来做通信。换句话说升级 https 只是把混合内容问题换成了纯正的跨域问题但跨域问题是可以用代码安全解决的所以这一步值得做。5.2 内网自签证书的信任问题很多内网系统没有公网域名只有10.x.x.x或者类似的内网 IP。给这种地址申请公网证书通常拿不到你自然会想到自签证书。但自签证书有个现实问题浏览器不认访问时会出现红色警告页用户无法直接进入。你是没法在 iframe 里跳过警告的因为父页面是 https子 iframe 加载自签证书站点也会被拦。解决思路只有两条。一条是在公司内部搭建私有 CA通过组策略或 MDM 把根证书批量装进员工电脑这样内网 https 可以被信任另一条是用内网穿透、内部 DNS 加泛域名证书的玩法本质上是让目标服务有一个可被信任的证书链。这两种方案都不算复杂但要看公司的基础设施条件。如果没有统一证书下发能力我建议还是回到反向代理方案至少不需要动每台电脑的证书信任库。5.3 升级后目标站内部的混合内容问题把目标服务从 http 升级到 https 之后一定要检查它内部引用的子资源。很多老系统页面里写死了http://cdn.example.com/js/app.js或者http://api.example.com/getData这些请求在 https 页面里会被拦截。常见表现是页面能打开但样式全丢、功能不可用、接口数据为空。在升级之前可以用爬虫或者简单脚本扫描一遍页面里的资源引用。也可以直接在目标站套一层 https然后用浏览器的 Network 面板看有没有红色报错。我见过一个团队把接口升级成 https 后前端页面里还有几十个图片是 http 的结果页面能打开但商品图全裂最后查了半天才意识到是自动升级失败而非图片文件丢失。6. 嵌入成功后的跨域通信postMessage、函数调用与高度自适应6.1 分清同源代理与跨域直连两种模式解决了混合内容之后你会面对两种不同的嵌入模式通信方式完全不一样。如果采用第 4 章的反向代理方案iframe 和父页面最终是同源的因为它们都跑在ops.example.com下。同源模式下父页面可以直接访问 iframe 的contentDocument可以读取 DOM、调用内嵌函数几乎没有限制。这种模式适合目标站就是你自己的老系统你信任它而且不想为通信写太多胶水代码。如果采用第 5 章的直连 https 方案iframe 和父页面很可能不同源。跨域模式下父页面拿不到 iframe 的 DOM所有通信只能通过postMessage异步传递。这种模式更像是把第三方系统作为一个黑盒嵌入双方各自维护一套消息协议。我在实际项目里的选择标准很简单目标是自家系统我就用同源代理目标是第三方系统或者双方需要严格隔离权限我就用跨域直连加 postMessage。6.2 postMessage完整示例父页和iframepostMessage 的完整链路分两步发送方调用postMessage携带消息和目标源接收方监听message事件并校验来源。父页面侧代码const frame document.getElementById(monitor-frame); function sendToFrame(type, payload) { frame.contentWindow.postMessage( { type, payload }, https://target.example.com ); } frame.addEventListener(load, () { sendToFrame(init, { token: localStorage.getItem(token) }); }); window.addEventListener(message, (event) { if (event.origin ! https://target.example.com) { return; } const { type, payload } event.data; console.log(iframe 消息:, type, payload); });iframe 内部页面侧代码window.addEventListener(message, (event) { if (event.origin ! https://ops.example.com) { return; } const { type, payload } event.data; if (type init) { // 用父页面传过来的 token 初始化业务 window.parent.postMessage( { type: ready, payload: { version: 1.0 } }, event.origin ); } });这里有一个常被忽视的细节postMessage的第二个参数是目标源不要图省事写*。写*意味着任何窗口都能收到你的消息极端情况下会把自己的数据发给恶意页面。两个端的event.origin校验也不能省否则你无法区分消息到底是从哪个站点发来的。6.3 父页面能不能直接调用iframe的函数同源能跨域不能这是我很经常被问到的问题主页面可以调用 iframe 的函数吗。分两种情况同源可以。父页面用iframe.contentWindow.someFunction()直接调用前提是 iframe 内部函数挂在了 window 上且 iframe 已经加载完成。跨域不行。浏览器同源策略直接禁止跨域窗口之间互相访问contentWindow的属性和函数强行访问会抛SecurityError。跨域场景只能靠 postMessage 触发对方监听器让对方自己执行函数。我在一个老项目里见过一个反面案例父页面在window.onload时直接调iframe.contentWindow.initChart()本地联调好好的一上生产就报错。原因就是本地同一个 localhost 端口算同源生产环境域名不同变成跨域函数调不到。后来改成 iframe 内部监听消息再执行initChart问题才解决。这段经历也让我养成了一个习惯写 iframe 交互代码之前先问自己能不能保证同源不能保证就老老实实用 postMessage。6.4 高度自适应的常用解法iframe 嵌入后比较烦的一个问题是高度写死。内部页面内容一变长要么出现滚动条要么裁切掉一部分。我推荐一套简单可靠的方案iframe 内部页面在内容尺寸变化时通过 postMessage 把新的高度告诉父页面父页面动态更新 iframe 高度。父页面监听window.addEventListener(message, (event) { if (event.origin ! https://target.example.com) { return; } if (event.data.type resize) { document.getElementById(monitor-frame).style.height event.data.height px; } });iframe 内部用ResizeObserver监听 body 高度new ResizeObserver((entries) { const height document.documentElement.scrollHeight; window.parent.postMessage({ type: resize, height }, https://ops.example.com); }).observe(document.body);这套方案比定时轮询高度要可靠得多因为 ResizeObserver 可以捕捉到 DOM 变化、图片加载、折叠面板展开等各类高度变化。实际用下来唯一的额外成本就是双方要约定消息格式。消息协议我习惯统一一个字段叫type这样后续扩展其他事件时不会冲突。7. 后端代理模式与产品层面的能不能嵌问题7.1 如果目标只有接口完全不需要iframe有些场景下你想嵌的根本不是别人的页面而是别人提供的数据接口。比如对方有一个http://10.0.0.8:8080/api/stats你想在自己的 https 后台里展示统计指标。这时候我不太推荐你去解决 iframe 混合内容问题因为完全有更轻的做法让后端出一个聚合接口后端去请求对方的 http 接口拿到数据返回给前端。前端代码变成普通的 fetch 调用不在页面里创建 iframe自然也就不存在混合内容被拦截的问题。后端之间互相访问 http 没有任何浏览器安全策略限制只要网络通就行还能在服务端做超时控制、数据清洗、接口鉴权、缓存。这个方案唯一的缺点是实时性不如 iframe 直观但绝大多数展示型场景完全够用。我遇到过的情况是业务方非要实时看对方系统的完整操作界面不想只统计数据这才需要走 iframe 方案。所以先问清楚需求再决定技术路线。7.2 纯前端为什么没有真正绕过mixed content的办法经常有同事问我能不能在页面里用 JS 判断一下协议如果发现是 http就自动转成 https听起来好像可以因为很多服务其实支持 https只是前端代码里写死了 http。这种try https first, then fallback的思路对图片类资源有效但用在 iframe 上非常不靠谱。另外一个常见的想法是用 Service Worker 把 http 请求改写成 https。Service Worker 确实能拦截部分请求但这里有个逻辑矛盾Service Worker 本身只能运行在 https 页面或 localhost 下它拦截的请求可以改写 URL但它无法让一个本来就不支持 https 的目标站凭空变成可访问的 https 站点。如果目标服务不支持 https改写也只是把请求发到一个不存在的端口上。还有人在搜索的时候会看到--allow-running-insecure-content之类的浏览器启动参数这是 Chromium 提供的一个调试开关。它确实可以允许加载混合内容但它是作用在浏览器进程级别的全局开关意味着这台浏览器访问的所有站点都不再检测混合内容生产环境用这个等于把整个站点的安全防护都关了。我只建议在本地调试目标站样式时临时用一下一旦上生产必须立刻停掉。7.3 对方拒绝被嵌入X-Frame-Options与CSP frame-ancestors前面几章都在解决能不能加载但还有一种情况是对方站点了安全策略主动拒绝被 iframe 嵌入。打开 DevTools 会看到Refused to display https://target.example.com in a frame because it set X-Frame-Options: DENY.这意味着即使你把 iframe 的 src 改成了 https、反代配置完美依然白屏。解决这个问题的唯一正确方式是联系目标站点管理员让他们放行你的域名。对应的配置是删除X-Frame-Options或改为ALLOW-FROM https://ops.example.com注意这个值兼容性不好设置 CSP 指令Content-Security-Policy: frame-ancestors https://ops.example.com我之前接一个集成项目对方是一个商业协作软件技术方案全都做好了最后卡在对方安全团队不允许任何外部站点嵌入沟通了两周才拿到白名单。从那以后我每次做 iframe 集成的第一件事是先看对方的响应头里有没有X-Frame-Options和Content-Security-Policy而不是急着写代码。读者可以先用curl -I看一眼curl -I https://target.example.com如果返回头里带了X-Frame-Options或者content-security-policy: frame-ancestors先别高兴提前评估能不能让对方改。8. 排查mixed content的实用套路8.1 DevTools里的关键信息怎么看遇到 iframe 白屏第一件事不是看页面而是打开 DevTools 的 Console。混合内容报错有非常明显的标识Mixed Content: ...。看报错时注意两点报错里提到的是 iframe 本身的请求还是 iframe 内部某个子资源的请求如果是后者iframe 的 src 可能已经是 https但内部还引用 http 的图片或接口。Network 面板也不可少。刷新页面右键过滤框里输入mixed能看到所有被标记为混合内容的请求。Chrome 会把被拦截的请求标成红色并在解释文字里写明原因。我习惯的做法是在 Network 面板里看请求的状态如果是canceled或blocked:mixed-content基本可以确认是混合内容拦截如果是普通的 200那白屏的原因就要往 JS 报错、接口异常、目标站被拒绝嵌入等方向排查。8.2 别把CORS错误当成混合内容错误iframe 场景里CORS 错误和混合内容错误非常容易混淆。混在 console 里的完整报错常常是一个 iframe 加载了一个 http 地址然后又因为内部 fetch 跨域报错两段报错叠在一起新手容易直接去配Access-Control-Allow-Origin。有一条判断经验如果 Console 里的报错以Mixed Content:开头优先处理混合内容如果报错是No Access-Control-Allow-Origin header is present on the requested resource但请求的 URL 是 https那才是真正的 CORS 问题。另外如果用反向代理把 iframe 变成了同源地址CORS 问题通常会同步消失因为同源请求不触发 CORS 机制。8.3 上线前检查清单最后整理一份我在上线前会逐条勾选的清单按顺序执行可以省去大部分返工先用curl -I看目标站响应头确认没有X-Frame-Options和frame-ancestors限制。确认 iframe 最终使用的 src 地址是 https。如果走了反向代理验证proxy_pass的斜杠规则避免 404。如果目标站内部有绝对路径 http 资源检查 Nginx 是否要做sub_filter或调整应用配置。如果依赖登录态确认 Cookie 的Secure、SameSite属性不会导致 iframe 内部请求丢失会话。如果页面里需要 iframe 与父页通信确认两边的origin全部是 https 且非null。在真实生产环境用 DevTools 重新看一遍 Console不要只看本地环境。我见过太多问题出在第 2 步前端以为配置了反代就能直接用结果 iframe 的 src 还是老代码里的 http 地址。这种改一行代码就能解决的事却要花掉二十分钟排查。所以清单里第 2 条我每天都提醒自己先确认地址栏里的 URL 是百分百 https再谈其他。最后分享一个个人习惯遇到 https 页面里任何子资源加载异常我从来不在浏览器设置层面找答案而是先把这个资源最终在浏览器里呈现的 URL 是什么这个问题弄清楚。地址决定了它会不会触发混合内容会不会触犯同源策略会不会被目标站安全策略拒绝。这三个维度排查完绝大多数 iframe 白屏问题都能在一刻钟内定位到根因。