ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

iframe跨域通信与鉴权实战:从postMessage到多端适配

iframe跨域通信与鉴权实战:从postMessage到多端适配 先说一个现实场景做前端这些年iframe是我又爱又恨的技术。爱的是页面隔离足够干净恨的是只要涉及跨域通信和鉴权坑就一个接一个。最近在做一个SaaS主站集成外部BI报表系统的项目同时还要适配PC浏览器和移动端H5整个链路把iframe的多端通信和鉴权问题完整走了一遍。这篇博文就把这套实战经验从原理到落地、从代码到避坑全部整理出来希望能给正在做iframe嵌套、跨域通信、以及被鉴权问题折磨的同行一点参考。1. 先想清楚为什么用iframe多端通信和鉴权到底难在哪1.1 一个典型的iframe集成场景讲通信和鉴权之前先还原一个具体的场景不然很容易纸上谈兵。假设你在做的是一个业务管理平台需求方提出要把第三方报表系统嵌入到主站的某个管理页面里。这个报表系统部署在自己的域名下主站是另一个域名用户已经在主站完成登录现在打开嵌入了报表的页面报表系统需要知道当前用户是谁、有没有权限看这些报表。这就产生了两个核心问题一是父页面里的用户态信息怎么安全地传给iframe里的报表系统二是报表系统内部的后续API请求怎么完成鉴权。这个场景非常典型做前端的人几乎都会遇到只是换了层壳。另外一类常见场景是微前端改造老系统没法快速重构只能用iframe先圈养起来这些老应用和主应用之间同样面临通信和鉴权的问题。所谓多端往大了说还包括PC浏览器、手机浏览器、移动端App内嵌WebView不同端对Cookie、localStorage、postMessage的支持程度不一样同一套代码在不同端上的表现可能完全不同。1.2 同源与跨域的本质区别Iframe通信的所有难度本质上都来自浏览器的同源策略。同源指的是两个页面的协议、域名、端口三者全部相同。只要同源父页面拿到iframe的contentWindow就可以直接访问里面的DOM、全局变量反过来iframe里也可以直接用window.parent访问父页面甚至互相调用函数这就是html iframe 内部调用外部这类需求的基础。但一旦跨域这些直接访问几乎全部被浏览器拦截只能走消息通道。很多新手不理解为什么非要用postMessage直接parent.someFunction()不行吗答案是不行。浏览器会抛出一个类似Blocked a frame with origin ... from accessing a cross-origin frame的错误。这不是框架限制是浏览器安全策略的执行任何语言和框架都绕不过去。理解这一点很重要因为后面所有方案设计的出发点都是在这道安全边界内做文章而不是试图突破它。1.3 鉴权在多端环境下的特殊麻烦iframe场景下的鉴权比普通页面麻烦根本原因是身份凭证跨了域。普通页面里登录之后浏览器保存Cookie后续请求自动带上一切看起来岁月静好。但在iframe里外部报表系统和主站域不同主站给用户发的Cookie不会自动出现在报表系统发起的请求里报表系统也读不到主站的localStorage。那报表系统如何确认当前请求者是已登录用户传统思路是把token通过某种方式传给iframeiframe再把它存到自己域下的localStorage或者内存里后续请求带上。这个思路大方向没问题但细节里全是坑token放在URL上传递会被服务器日志、浏览器历史记录泄漏postMessage传递要防止被恶意页面伪造消息Cookie模式下又面临第三方Cookie被浏览器禁用的困境。这些坑我在项目里都踩过一个一个说。2. 通信层实现postMessage是唯一靠谱的跨域桥梁2.1 父页面与iframe之间的双向往来postMessage是HTML5引入的跨文档消息通信接口专门解决跨窗口、跨iframe的消息传递问题。用法两端都一样发送端调用目标窗口的postMessage接收端通过message事件监听。我直接给出父页面发消息给iframe的代码。// 父页面向 iframe 发送初始化消息 const iframeWin document.getElementById(reportFrame).contentWindow; iframeWin.postMessage({ type: AUTH_INIT, payload: { ticket: one-time-ticket, userId: u_10086, ts: Date.now() } }, https://bi.example.com);注意第二个参数这里填的是iframe的真实来源地址不是。填意味着消息会发到任何加载在该窗口里的页面包括被劫持的恶意页面填具体origin浏览器会确认目标窗口的来源确实匹配才发送不匹配直接丢弃。安全编码的核心习惯就是这里养成的能写具体origin永远不要图省事写*。2.2 targetOrigin和event.origin两个必修参数接收端也有一个对应的关键参数就是event.origin。这段代码是iframe内部接收父页面消息的标准写法。// iframe 内监听父页面消息 window.addEventListener(message, function(event) { // 第一步永远校验来源 if (event.origin ! https://app.example.com) { console.warn(收到来自未知来源的消息:, event.origin); return; } const data event.data || {}; if (data.type AUTH_INIT data.payload) { // 先让父页面确认收到了 event.source.postMessage({ type: AUTH_ACK, status: ok }, event.origin); // 保存票据到 sessionStorage供后续业务请求使用 sessionStorage.setItem(auth_ticket, data.payload.ticket); } }, false);这里的event.source很有用它就是发消息的那个窗口对象可以直接给它回消息。有了它就不用在iframe内部保存父窗口的引用哪个窗口发来的就回给哪个窗口天然适配一个iframe被多个父窗口引用的场景。为什么一定要校验event.origin说个极端情况如果你的页面被嵌在一个恶意网站里对方通过iframe引入你的页面然后伪造一条AUTH_INIT消息发给你的iframe你的页面就把票据写进sessionStorage了。如果不校验来源就相当于把大门钥匙交到了来历不明的人手里。origin校验就是门卫先看清楚再放行这一步不能省。2.3 通信时序load事件、ready消息与初始化握手postMessage的第二个大坑是时序问题。父页面如果在iframe还没有加载完的时候发消息iframe内部的监听器可能还没注册消息就直接丢失而且没有任何报错。这是异步加载的典型竞态。推荐做法不是依赖load事件硬等而是在iframe内部主动向父页面发送我准备好了的消息由iframe发起一次握手。我的习惯是任何iframe通信都先做一次PING/PONG握手确认链路通了再传真实业务数据。// iframe 内部页面初始化完成后主动通知父页面 window.addEventListener(message, initHandler); function initHandler(event) { if (event.origin ! https://app.example.com) return; if (!event.data || typeof event.data ! object) return; if (event.data.type PING) { // 收到PING回应PONG然后进入认证流程 event.source.postMessage({ type: PONG, status: ready }, event.origin); handleAuthExchange(event); } } function handleAuthExchange(event) { // 向父页面索要一次性票据 event.source.postMessage({ type: AUTH_REQUEST }, event.origin); }父页面这边收到PONG之后才开始传真正的业务数据。这个PING/PONG虽然多了一个来回但能避免掉绝大多数的通信竞态问题尤其适合那种加载慢、初始化重的报表系统实测下来非常稳。3. 鉴权层设计token怎么从父页安全地交给iframe3.1 不推荐的做法URL参数携带token先说不推荐方案因为很多项目图省事直接在iframe src后面拼tokeniframe srchttps://bi.example.com/report?tokenxxx /这样做真正危险的地方在于token会出现在多个地方浏览器历史记录、服务器访问日志、代理日志、页面源码。哪怕你只放一次性票据风险依然很高。尤其是如果票据有效期较长一旦URL被复制分享谁拿到完整URL谁就能以当前用户身份访问系统这是非常严重的安全隐患。另外iframe的src如果动态拼接token每次用户刷新主页面iframe都会重新加载内部状态全部丢失对BI报表这类重交互应用来说体验非常差。搜索热词里有鉴权绕过这个方案其实就是最典型的可绕过路径只要URL被泄漏等于身份被绕过。3.2 推荐做法postMessage传递票据iframe内业务请求携带我实际落地的方案是这样的父页面内用户完成登录后主站后端为iframe嵌入场景签发一次性票据ticket有效期控制在1分钟以内。父页面加载iframeiframe内部通过握手流程向父页面索要票据。父页面通过postMessage把票据传给iframe同时带上用户ID和过期时间。iframe拿到票据后立刻调用自己后端的兑换接口把一次性票据换成自己域下的正式会话自己的JWT或Cookie。之后iframe内所有业务请求都走它自己的正式会话不再依赖父页面传过来的任何东西。第4步是整个方案的核心票据只走一次性换来的会话留在iframe自己域内。这样即使票据在通信过程中被截获也无法二次使用而且票据的有效期极短。iframe后端兑换时还要校验签发方只有来自主站后端签发的票据才接受。// iframe 内部兑换票据并初始化业务请求 async function exchangeTicket(ticket) { const resp await fetch(https://bi.example.com/api/auth/exchange, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ ticket }) }); if (!resp.ok) { // 兑不了票说明票据过期或无效通知父页面重新签发 window.parent.postMessage({ type: AUTH_EXPIRE }, https://app.example.com); return null; } const { accessToken } await resp.json(); // 存到内存或 sessionStorage后续请求带上 sessionStorage.setItem(bi_access_token, accessToken); return accessToken; }从安全角度讲这个设计把一次性票据和长期会话做了明确区分。一次性票据只负责过桥长期会话只存在于iframe自己的域里由iframe自己的后端签发两边各管一段最大程度减少了信任传递的半径。3.3 服务端配合一次性票据、state校验、cookie samesite前端这套动作如果没有后端配合安全性就是空谈。一共三个关键点必须落实。第一票据必须是一次性的。用户在主站登录后每次进入报表页面主站后端都签发新的一次性票据并记录票据和用户名、嵌入页面会话标识的绑定关系。BI后端兑换时不仅要验证票据签名还要确认这个票据没被使用过用完立即作废。这个设计可以抵御日志泄漏、postMessage通道被第三方页面伪造等风险。第二强烈建议在票据里绑定一个state参数。这个state由父页面随机生成并随票据一起传给iframeiframe兑换时把state也提交给BI后端BI后端拿着state回调主站后端校验。这个流程类似OAuth 2.0里state参数的防CSRF机制能防止有人拿着盗来的票据去BI系统换会话。第三如果父页面和iframe恰好同域可以考虑用Cookie配合SameSite属性但从Chrome 80开始Lax是默认值第三方上下文里发Cookie必须显式设为None并加上Secure。不过绝大多数真实跨域场景里Cookie方案在iframe里经常碰壁所以更推荐token加postMessage的组合。你可以把Cookie方案当成同域场景的简化版但跨域场景千万别依赖它。3.4 防止鉴权绕过和点击劫持的关键配置鉴权绕过这个热词点出了iframe场景最现实的安全问题。最常见的绕过路径有三类第一类是直接篡改iframe的src把原本预期的外部页面换成攻击者自己的页面诱导父页面把身份信息发过去。对应的防御手段就是前面强调的postMessage的targetOrigin写死成固定域接收端的event.origin也必须严格校验不允许任何动态通配。第二类是点击劫持。攻击者用透明iframe覆盖在页面上诱导用户点击。防御方式是iframe内页面在响应头里加X-Frame-Options和CSP的frame-ancestors限制。X-Frame-Options: DENY直接禁止被任何页面嵌入想精细控制的话用Content-Security-Policy的frame-ancestors指令只允许信任的父页面嵌入。第三类是本地存储污染。iframe如果自己往localStorage里写了身份信息而同域下的其它页面也能读到这就扩大了攻击面。所以我把token放内存或sessionStorage尽量避免localStorage至少有效期要短。这里说的不是因为postMessage不可用而是任何持久化凭证都会随设备扩散sessionStorage至少在关闭标签页后是干净的。4. 多端适配细节PC浏览器、移动端H5、App WebView的差异4.1 PC浏览器端第三方Cookie被禁的连锁反应这个点很多人容易忽略直到线上事故才想起来。PC端浏览器尤其是Safari和Chrome近几年对第三方Cookie的限制越来越严格。在iframe场景中iframe内发起的跨域请求如果试图自动携带Cookie也就是第三方Cookie在Safari里默认是禁止的。结果就是iframe服务端虽然建立了会话但浏览器不存会话Cookie后续请求一直处于未登录状态报表系统反复跳登录页。解决办法就是回到token方案不依赖Cookieiframe内部的请求统一用Authorization头带token这样就跟第三方Cookie策略完全解耦。如果你的iframe系统虽然跨域但同属一个主域名比如app.example.com和bi.example.com可以尝试把Cookie的Domain设为.example.com但Safari的ITP策略对脚本写入的Cookie同样有限制所以总体来说iframe的鉴权凭证不要依赖Cookie显式走请求头最稳。4.2 移动端H5与App WebView的差异与桥接方案到了移动端场景会更复杂。先看手机浏览器里的H5页面行为跟PC浏览器基本一致postMessage、sessionStorage这些API都可用但需要注意iOS WKWebView对localStorage的同域限制比较严格。如果H5页面和iframe的域不一致localStorage完全隔离用postMessage传递票据反而更干净正好绕开这个限制。再看App内嵌WebView。如果是自家App的WebView通常可以借助原生桥接实现一些浏览器不够直接的能力。通信链路可以是App通过原生桥注入一个JavaScript方法H5页面调用这个方法把票据传给原生原生再通过evaluateJavascript把App侧的信息传回JS。这个过程和postMessage是并行的两者可以互补。实际操作时我建议把通信模块抽象成一个适配层对外暴露sendMessage和onMessage两个接口内部判断当前环境是PC浏览器、H5还是App WebView分别走postMessage或原生桥上层业务完全不用感知差异。这样做最大的好处是后续换容器、加端只需要改适配层不用动业务代码。4.3 UA识别与降级方案多端适配里还必须处理UA识别。在移动端很多时候需要知道当前是否在微信浏览器、支付宝浏览器或某个App内这类信息浏览器不会提供标准API只能从UA字符串里解析。所以适配层里通常会包含一个环境识别函数比如判断isWeChat、isAlipay、isAppWebView然后针对不同环境做细微的策略调整。降级方案也值得准备如果postMessage不可用极老内核浏览器或者某些WebView的奇怪实现需要能退回到从URL参数读取初始化参数虽然安全性弱一些但至少保证功能可用。我的做法是在适配层里检测window.postMessage是否存在不存在就自动读取location.search里的参数。这样老环境不挂掉新环境走安全通道。做这一行的都知道支持老环境有时不是怕兼容性测试而是怕客户现场环境不能升级。5. 实战排查通信失败和鉴权失效的定位套路5.1 问题定位的通用流程iframe的通信和鉴权问题排查最怕乱猜。我整理了一套固定排查流程基本能覆盖80%的情况。第一步先看iframe有没有加载成功。打开开发者工具Network面板看iframe对应文档的请求状态。如果返回403或者被X-Frame-Options拦截问题在服务端响应头而不是前端代码。第二步在控制台执行document.getElementById(reportFrame).contentWindow看能不能拿到窗口对象。拿不到说明跨域限制异常可能是被浏览器安全策略拦截。第三步在父页面和iframe内各写一个message监听器把每次收到的event.origin和event.data都打印出来。这一步能立刻定位是消息压根没发出去还是发出去了但被这边拦截了。第四步检查时序确认iframe内部的初始化代码执行了没有、接收监听器注册了没有。5.2 常见问题速查表我把实战中频率最高的问题整理成一个表方便快速定位现象可能原因处理方式iframe页面一直转圈不显示服务端X-Frame-Options或CSP frame-ancestors拦截检查响应头确认父页面域被允许嵌入父页面postMessage发了没反应iframe内部监听器未注册完成改用PING/PONG握手后再发业务消息消息收到但event.origin为空部分浏览器对file://协议下的origin处理不同统一用http/https访问不要用file://调试iframe内请求一直401票据未兑换成功或已失效看兑换接口返回值检查一次性票据是否被重复使用Safari下iframe请求不带Cookie第三方Cookie被ITP禁用改用Authorization头传递token换个设备就出现鉴权失败localStorage在不同端数据不同步确保每次进入都通过postMessage重新获取票据5.3 开发者工具里的iframe调试技巧Chrome开发者工具其实有专门的iframe调试能力。Elements面板里iframe是一个独立的文档树可以直接在里面选中元素Console的上下文默认是顶级页面想访问iframe内部变量时要么用document.getElementById(reportFrame).contentWindow访问要么用工具栏里的JavaScript context下拉菜单切换到这个iframe切过去之后就像直接在iframe内部调试一样。还有个小技巧Network面板默认只显示顶级页面的网络请求要单独看iframe内部的请求可以在过滤器里输入domain:bi.example.com就把这个域下的所有请求筛出来了。排查401、504这类问题配合status-code:401一起过滤定位非常快。调试消息通信时还可以活用console的过滤功能在message监听器里只输出带特定字段的日志避免被无关消息刷屏。6. 这套方案在真实项目里的效果和体会这套方案在我自己的项目里已经跑了大半年期间经历了好几次Safari升级、用户换设备、App发版这些外部变化基本上没有因为通信和鉴权本身出过大问题。最直观的感受是通信和鉴权一旦稳定下来整个iframe集成就稳了一大半剩下的都是业务细节。要说体会最核心的一条是凡是涉及跨域、涉及可信边界的代码默认都不可信。postMessage的接收端要校验originiframe的src要固定来源票据必须一次性这些不是形式主义是从一个个线上事故里换来的教训。最后分享一个小技巧调试iframe嵌入场景时别急着写业务逻辑先把PING/PONG握手和日志输出跑通把整条链路点亮。链路通了通信和鉴权的问题都会变得非常直白一看就知道卡在哪一步。这个习惯帮我省了大量排查时间也推荐给你。
返回列表