【知识点】关于iframe跨域通信

3,854 阅读14分钟

一、前言

中秋前的需求迭代,遇到一个iframe跨域通信的问题,域名B的一个接口请求里的字段用到里域名A下的账户id,原本的代码是直接用sessionStorage.getItem(info)的方式从域名A的缓存中获取用户信息,结果没有拿到。

于是,我脑海里回忆iframe相关的知识点,想到不同域名下的信息是无法直接获取的。没错,这个本应该熟悉的iframe基础知识点,我靠几分钟的回忆才想起来。改完需求之后,我想我是时候重拾一下iframe的相关知识了。

以下关于iframe的介绍主要来自MDN

二、基础信息

,它是HTML内联框架元素,表示嵌套的browsing context。它能够将另一个HTML页面嵌入到当前页面中。 每个嵌入的浏览上下文(embedded browsing context)都有自己的会话历史记录(session history)和DOM树。包含嵌入内容的浏览上下文称为父级浏览上下文。顶级浏览上下文(没有父级)通常是由 Window 对象表示的浏览器窗口。

三、属性

3.1 allow

用户为指定特征策略。

3.1.1特征策略

简单介绍一下特征策略,特征策略允许web开发者在浏览器中选择启用、禁用和修改确切特征和 API 的行为.比如内容安全策略,但是它控制的是浏览器的特征非安全行为。allow是iframe特有的特征策略,可以控制iframe使用哪些特征。

特征策略的语法

<allowlist>

*: 本特性默认在最上层和包含的内容中(iframes)允许。

'self': 本特性默认在最上层允许,而包含的内容中(iframes)使用源地址相同设定。也就是说本特性在iframe中不允许跨域访问。

'none': 本特性默认在最上层和包含的内容中(iframes)都禁止。

'src': (只在iframe中允许) 只要在src 中的URL和加载iframe用的URL相同,则本特性在iframe中允许。

其中*或'none'只允许单独使用,而'self'和'src'可以与多个源地址一起使用。

3.1.2 语法

禁止全屏模式

可以设置fullscreen的特征策略为'none'。

<iframe allow="fullscreen 'none'">

支持多个特征策略

禁止全屏模式和禁止调起支付接口,多个用分号隔开。

<iframe allow="fullscreen 'none'; payment 'none'">

3.2 csp

对嵌入的资源配置内容安全策略

内容安全策略( CSP )

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS (en-US)) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

设置内容安全策略

比如一个引入的css文件,如果我们设置了如下策略,该策略禁止任何资源的加载,除了来自cdn.example.com的样式表。

head中设置安全策略并引入外部的css文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'cdn.example.com'; " />
    <title>iframe-child</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    ...
  </body>
</html>

这个时候如果在浏览器打开页面,打开开发者工具就能看到报错:

Refused to load the stylesheet 'file:///Users/Desktop/weblearn/iframe/style.css' because it violates the following Content Security Policy directive: "style-src 'cdn.example.com'". Note that 'style-src-elem' was not explicitly set, so 'style-src' is used as a fallback.

这表示样式表仅允许加载自cdn.example.com,然而该页面企图从自己的源 (example.com) 加载。

3.3 height

以CSS像素格式HTML5,或像素格式HTML 4.01,或百分比格式指定frame的高度。默认值为150。

3.4 importance

表示 <iframe> 的 src 属性指定的资源的加载优先级。允许的值有:

auto (default):不指定优先级。浏览器根据自身情况决定资源的加载顺序

high:资源的加载优先级较高

low:资源的加载优先级较低

3.4 name

用于定位嵌入的浏览上下文的名称。该名称可以用作 <a> 标签与 <form> 标签的 target 属性值,也可以用作 <input> 标签和 <button> 标签的 formtarget 属性值,还可以用作 window.open() 方法的 windowName 参数值。

用作 <a> 标签的 target 属性值

<iframe name="myIframe" src="http://www.w3cschool.cc" width="500" height="200"></iframe>
<a href="https://juejin.cn/" target="myIframe">掘金</a>

点击掘金按钮前展示:

image.png

点击掘金按钮后展示:

image.png

在iframe窗口中打开页面

使用window.open()打开窗口,该方法的第二个参数可以指定窗口名称,我们通过window.name可以拿到iframe的name,赋值到window.open()中。

父页面:

<iframe name="myIframe" src="./child.html" width="500" height="200"></iframe>

iframe页面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div>
      <input type="button" id="open" value="打开弹窗" />
    </div>
    <script>
      window.onload = function () {
        var div = document.querySelector('div');
        document.querySelector('#open').onclick = function () {
          window.open('https://juejin.cn/', window.name);
        };
      };
    </script>
  </body>
</html>

可以在当前iframe窗口中打开掘金的页面:

image.png

3.5 referrerpolicy

表示在获取 iframe 资源时如何发送 referrer 首部:

no-referrer: 不发送 Referer 首部。

no-referrer-when-downgrade (default): 向不受 TLS (HTTPS) 保护的 origin 发送请求时,不发送 Referer 首部。

origin: referrer 首部中仅包含来源页面的源。换言之,仅包含来源页面的 scheme, host, 以及 port (en-US)。

origin-when-cross-origin: 发起跨域请求时,仅在 referrer 中包含来源页面的源。发起同源请求时,仍然会在 referrer 中包含来源页面在服务器上的路径信息。

same-origin: 对于 same origin (同源)请求,发送 referrer 首部,否则不发送。

strict-origin: 仅当被请求页面和来源页面具有相同的协议安全等级时才发送 referrer 首部(比如从采用 HTTPS 协议的页面请求另一个采用 HTTPS 协议的页面)。如果被请求页面的协议安全等级较低,则不会发送 referrer 首部(比如从采用 HTTPS 协议的页面请求采用 HTTP 协议的页面)。

strict-origin-when-cross-origin: 当发起同源请求时,在 referrer 首部中包含完整的 URL。当被请求页面与来源页面不同源但是有相同协议安全等级时(比如 HTTPS→HTTPS),在 referrer 首部中仅包含来源页面的源。当被请求页面的协议安全等级较低时(比如 HTTPS→HTTP),不发送 referrer 首部。

unsafe-url: 始终在 referrer 首部中包含源以及路径 (但不包括 fragment,密码,或用户名)。这个值是不安全的, 因为这样做会暴露受 TLS 保护的资源的源和路径信息。

不添加referrerpolicy

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" />

可以在请求中看到referrer的来源:

image.png

no-referrer

将referrerpolicy的值设置为no-referrer

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" referrerpolicy="no-referrer" />

我们在请求中会看不到referrer的来源:

image.png

origin

将referrerpolicy的值设置为origin

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" referrerpolicy="origin" />

可以在请求中看到referrer的来源:

image.png

origin-when-cross-origin

将referrerpolicy的值设置为origin-when-cross-origin

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" referrerpolicy="origin-when-cross-origin" />

非同源请求的referrer的来源:

image.png

same-origin

将referrerpolicy的值设置为same-origin

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" referrerpolicy="same-origin" />

非同源请求看不到referrer的来源:

image.png

<iframe name="myIframe" src="http://localhost:8888/test/iframe" width="500" height="200" referrerpolicy="same-origin" />

同源请求可以看到referrer的来源:

image.png

strict-origin-when-cross-origin

将referrerpolicy的值设置为strict-origin-when-cross-origin

<iframe name="myIframe" src="http://localhost:8888/test/iframe" width="500" height="200" referrerpolicy="strict-origin-when-cross-origin" />

同源请求可以看到referrer的来源中有完整的路径:

image.png

unsafe-url

将referrerpolicy的值设置为unsafe-url

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200" referrerpolicy="unsafe-url" />

同源请求可以看到referrer的来源中有源、完整路径等:

image.png

3.6 sandbox

该属性对呈现在 iframe 框架中的内容启用一些额外的限制条件。属性值可以为空字符串(这种情况下会启用所有限制),也可以是用空格分隔的一系列指定的字符串。有效的值有:

allow-downloads-without-user-activation : 允许在没有征求用户同意的情况下下载文件.

allow-forms: 允许嵌入的浏览上下文提交表单。如果没有使用该关键字,则无法提交表单。

allow-modals: 允许嵌入的浏览上下文打开模态窗口。

allow-orientation-lock: 允许嵌入的浏览上下文锁定屏幕方向(译者注:比如智能手机、平板电脑的水平朝向或垂直朝向)。

allow-pointer-lock: 允许嵌入的浏览上下文使用 Pointer Lock API.

allow-popups: 允许弹窗 (例如 window.open, target="_blank", showModalDialog)。如果没有使用该关键字,相应的功能将自动被禁用。

allow-popups-to-escape-sandbox: 允许沙箱化的文档打开新窗口,并且新窗口不会继承沙箱标记。例如,安全地沙箱化一个广告页面,而不会在广告链接到的新页面中启用相同的限制条件。

allow-presentation: 允许嵌入的浏览上下文开始一个 presentation session。

allow-same-origin: 如果没有使用该关键字,嵌入的浏览上下文将被视为来自一个独立的源,这将使 same-origin policy 同源检查失败。

allow-scripts: 允许嵌入的浏览上下文运行脚本(但不能创建弹窗)。如果没有使用该关键字,就无法运行脚本。

allow-storage-access-by-user-activation : 允许嵌入的浏览上下文通过 Storage Access API 使用父级浏览上下文的存储功能。

allow-top-navigation: 允许嵌入的浏览上下文导航(加载)内容到顶级的浏览上下文。

allow-top-navigation-by-user-activation: 允许嵌入的浏览上下文在经过用户允许后导航(加载)内容到顶级的浏览上下文。

3.7 src

被嵌套的页面的 URL 地址。使用 about:blank 值可以嵌入一个遵从同源策略的空白页。在 Firefox (version 65及更高版本)、基于 Chromium 的浏览器、Safari/iOS 中使用代码移除 iframe 的 src 属性(例如通过 Element.removeAttribute() )会导致 about:blank 被载入 frame。

<iframe name="myIframe" src="https://juejin.cn/" width="500" height="200"></iframe>

iframe窗口中会展示src设置的掘金页面:

image.png

3.8 srcdoc HTML5 only

该属性是一段HTML代码,这些代码会被渲染到 iframe 中。如果浏览器不支持 srcdoc 属性,则会渲染 src 属性表示的内容。

3.9 width

以CSS像素格式HTML5,或以像素格式HTML 4.01,或以百分比格式指定的 frame 的宽度。默认值是300。

四、 脚本

内联的框架,就像 <frame> 元素一样,会被包含在 window.frames 伪数组(类数组的对象)中。

有了 DOM HTMLIFrameElement 对象,脚本可以通过contentWindow访问内联框架的window对象。 contentDocument属性则引用了 <iframe> 内部的 document 元素,(等同于使用contentWindow.document),但IE8-不支持。

在框架内部,脚本可以通过 window.parent引用父窗口对象。

脚本访问框架内容必须遵守同源策略,并且无法访问非同源的 window 对象的几乎所有属性。同源策略同样适用于子窗体访问父窗体的 window 对象。跨域通信可以通过 window.postMessage 来实现。

4.1 脚本通过contentWindow访问内联框架的 window 对象

在内嵌iframe页面,可以用脚本通过contentWindow访问内联框架的window对象,进而改变内联框架的样式,比如下面代码示例中修改内联框架的背景颜色和字体颜色:

iframe.html

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe</title>
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <div class="wrap">
      <div class="mb20 font-lg">Iframe</div>
      <input type="button" class="mb20" onclick="changeStyle()" value="修改背景颜色" />
      <!-- 本地页面会报同源错误 -->
      <iframe id="myIframe" src="http://127.0.0.1:3000/child/" width="500" height="200"></iframe>
    </div>
  </body>
  <script>
    function changeStyle() {
      var x = document.getElementById('myIframe');
      var y = x.contentWindow || x.contentDocument;
      if (y.document) y = y.document;
      y.body.style.backgroundColor = '#d80000';
      y.body.style.color = '#fff';
    }
  </script>
</html>

child.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div>
      改变背景颜色
    </div>
  </body>
</html>

UI

image.png

注:通过contentWindow改变内联框架的属性时,如果是本地页面直接在Chrome中打开,点击触发更改事件的时候会报错:

DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.

通过百度查询资料了解到这是由于Chrome同源策略引起的,只有在页面加载脚本时才会出现该问题,所以我使用node启动一个简单的Web服务器,通过本地服务再访问页面功能就可以了。贴出我的node.js代码:

// 目标:
// 浏览器中输入 http://127.0.0.1:3000/iframe/ 时,显示 iframe.html

//1. 加载 express 模块
const express = require('express');

//2. 创建服务器
const app = express();

//3. 启动服务器
app.listen(3000, () => {
  console.log('express-server is running...');
});

const path = require('path');

// 4. 监听路由
app.get('/iframe', (req, res) => {
  res.sendFile(path.join(__dirname, 'view', 'iframe.html'), err => {});
});

app.get('/child', (req, res) => {
  res.sendFile(path.join(__dirname, 'view', 'child.html'), err => {});
});

浏览器中输入 http://127.0.0.1:3000/iframe/,即可访问本地页面:

image.png

4.2 框架内部脚本可以通过window.parent访问父窗口对象

框架内部脚本可以通过window.parent访问父窗口对象,进而改变父窗口的样式

iframe.html

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe</title>
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <div class="wrap">
      <div class="mb20 font-lg">Iframe</div>
      <div class="box" id="box">父页面box</div>
      <!-- 本地页面会报同源错误 -->
      <iframe id="myIframe" src="http://127.0.0.1:3000/child/" width="500" height="200"></iframe>
    </div>
  </body>
</html>

child.html

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div>
      <input type="button" id="changeColor" value="改变父页面颜色" />
    </div>
    <script>
      window.onload = function () {
        var div = document.querySelector('div');
        document.querySelector('#changeColor').onclick = function () {
          var parent = window.parent.document.getElementById('box');
          parent.style.backgroundColor = '#d80000';
          parent.style.color = '#fff';
        };
      };
    </script>
  </body>
</html>

UI

image.png

五、定位和缩放

作为一个可替换元素, 可以使用 object-position 和 object-fit 来定位、对齐、缩放 元素内的文档。

六、iframe跨域通信

前面把iframe基本的知识点都过了一遍,加深了对iframe的了解。那么我们开头所讲的实际业务需求中遇到的问题,iframe的跨域通信来了。

讲跨域通信之前,我们先了解一下什么是跨域,以及常见的跨域场景都有哪些。

6.1 跨域

6.1.1 同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

如果两个 URL的协议、主机、端口元组都相同的话,则这两个 URL 是同源。

如果两个 URL的协议、主机、端口元组任意一个不同时,这两个URL不同源,不同源之间相互请求资源,称之为跨域通信。

6.1.2 跨域场景

下表给出了与 URL store.company.com/dir/page.ht… 的源进行对比的示例:

URL结果原因
store.company.com/dir2/other.…同源只有路径不同
store.company.com/dir/inner/a…同源只有路径不同
store.company.com/secure.html失败协议不同
store.company.com:81/dir/etc.htm…失败端口不同 ( http:// 默认端口是80)
news.company.com/dir/other.h…失败主机不同

6.2 iframe跨域通信解决方案

我们把内嵌iframe的页面称之为父页面,被嵌套页面为子页面,两个页面间需要传递数据,进而达成我们需要的功能,不同方向的通信,实现方式也不一样。

6.2.1 父页面向子页面传递数据

1.借助iframe标签的src元素

src的值为被嵌套的页面的URL地址,URL地址后面可以添加参数,嵌套页面可以通过window.location.href获取当前的URL地址,从而得到URL地址上携带的参数。例如:

iframe.html

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe</title>
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <div class="wrap">
      <div class="mb20 font-lg">Iframe</div>
      <iframe id="myIframe" src="./child.html?accountId=10"></iframe>
    </div>
  </body>
</html>

child.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div id="child"></div>
    <script>
      window.onload = function () {
        var div = document.querySelector('div');
        const params = window.location.href.split('?');
        const accountId = params[1].split('=')[1];
        var node = document.createElement('div');
        node.innerHTML = `当前账户id是${accountId}`;
        div.appendChild(node);
      };
    </script>
  </body>
</html>

UI

image.png

2.postMessage

先简单了解一下postMessage的功能,来源MDN

window.postMessage() 方法可以安全地实现跨源通信。

语法

otherWindow.postMessage(message, targetOrigin, [transfer]);

otherWindow

其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。

message

将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。[1]

targetOrigin

通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串""(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。

transfer 可选

是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

iframe.html

父页面通过postMessage向子页面通信,将targetOrigin设置为'*',不做同源限制。

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe</title>
  </head>
  <body>
    <div class="wrap">
      <div class="mb20 font-lg">Iframe</div>
      <iframe id="myIframe" src="http://127.0.0.1:3002/child/"></iframe>
    </div>
  </body>
  <script>
    const myIframe = document.getElementById('myIframe');
    myIframe.onload = function () {
      myIframe.contentWindow.postMessage('父页面发送的消息: 子页面url为http://127.0.0.1:3002/child/', '*');
    };
  </script>
</html>

child.html

子页面使用window.addEventListener监听父页面message事件,回调事件可以从event.data中获取到父页面传递的data。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div id="child">
      <div style="margin-bottom: 10px;">子页面展示:</div>
    </div>
    <script>
      // 监听父页面message事件
      window.addEventListener('message', receive, false);
      // 回调函数
      function receive(event) {
        // 传递的data可以从event.data中获取到
        var div = document.querySelector('div');
        var node = document.createElement('div');
        node.innerHTML = event.data;
        div.appendChild(node);
      }
    </script>
  </body>
</html>

UI

image.png

6.2.2 子页面向父页面传递数据

子页面通过也可以通过postMessage向父页面传递数据,不同的是otherWindow为父页面窗口,所以子页面需要使用window.parent表示父页面窗口(见4.2)。

child.html

子页面通过postMessage向父页面通信,将targetOrigin设置为'*',不做同源限制。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe-child</title>
  </head>
  <body>
    <div id="child"></div>
    <script>
      window.parent.postMessage('子页面发送的消息: 父页面url为http://127.0.0.1:3000/iframe/', '*');
    </script>
  </body>
</html>

iframe.html

父页面使用window.addEventListener监听子页面message事件,回调事件可以从event.data中获取到子页面传递的data。

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe</title>
  </head>
  <body>
    <div class="wrap">
      <div class="mb20 font-lg">Iframe</div>
      <div id="box">父页面展示:</div>
      <br />
      <iframe id="myIframe" src="http://127.0.0.1:3002/child/"></iframe>
    </div>
  </body>
  <script>
    // 监听子页面message事件
    window.addEventListener('message', receive, false);
    // 回调函数
    function receive(event) {
      // 传递的data可以从event.data中获取到
      var div = document.getElementById('box');
      var node = document.createElement('div');
      node.innerHTML = event.data;
      div.appendChild(node);
    }
  </script>
</html>

UI

image.png

七、总结

温故而知新,可以为师矣。古人诚不欺我。

通过再次学习iframe,对iframe了解加深,且对于之前时常记忆不深的跨域解决方案有了更深刻的记忆。

时不我待,感谢不断坚持的自己,加油!