1. 白屏现场:一个http地址在https页面里被静默干掉
先还原一个我最近被反复问到的场景:客户有一套内部运营后台,主域名已经切到 https,但某个老旧的监控系统还跑在内网 http 上。为了不让人来回切标签页,大家习惯把监控页面直接嵌进后台:
<iframe src="http://10.0.0.8:8080/monitor" width="100%" height="800"></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 地址 | http | http://10.0.0.8:8080/monitor | 正常渲染 |
| 普通 http 页面嵌入 | http | http://10.0.0.8:8080/monitor | 正常渲染 |
| https 页面嵌入 | https | http://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 自带边框,很多项目还要额外处理隐藏滚动条。传统做法是在标签上写scrolling="no",但 HTML5 里这个属性已经废弃,我通常用 CSS:
iframe { border: 0; overflow: hidden; width: 100%; height: 100%; }注意overflow: hidden只是让 iframe 自身不出现滚动条,如果内部页面内容超出高度,真正的内容会被裁切掉,这牵扯到高度自适应,后面我会专门讲。
4. 生产环境最稳方案:用Nginx反向代理把http"洗"成同源https
4.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完整示例(父页和iframe)
postMessage 的完整链路分两步:发送方调用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 白屏问题都能在一刻钟内定位到根因。