前言
中秋前的需求迭代,遇到一个 iframe 跨域通信的问题,域名 B 的一个接口请求里的字段用到里域名 A 下的账户 id,原本的代码是直接用 sessionStorage.getItem(info)的方式从域名 A 的缓存中获取用户信息,结果没有拿到。
于是,我脑海里回忆 iframe 相关的知识点,想到不同域名下的信息是无法直接获取的。没错,这个本应该熟悉的 iframe 基础知识点,我靠几分钟的回忆才想起来。改完需求之后,我想我是时候重拾一下 iframe 的相关知识了。
以下关于 iframe 的介绍主要来自MDN。
基础信息
<iframe>,它是 HTML 内联框架元素,表示嵌套的 browsing context。它能够将另一个 HTML 页面嵌入到当前页面中。
每个嵌入的浏览上下文(embedded browsing context)都有自己的会话历史记录(session history)和 DOM 树。包含嵌入内容的浏览上下文称为父级浏览上下文。顶级浏览上下文(没有父级)通常是由 Window 对象表示的浏览器窗口。
属性
allow
用户为<iframe>指定特征策略。
特征策略
简单介绍一下特征策略,特征策略允许 web 开发者在浏览器中选择启用、禁用和修改确切特征和 API 的行为.比如内容安全策略,但是它控制的是浏览器的特征非安全行为。allow 是 iframe 特有的特征策略,可以控制 iframe 使用哪些特征。
特征策略的语法
*: 本特性默认在最上层和包含的内容中(iframes)允许。
'self': 本特性默认在最上层允许,而包含的内容中(iframes)使用源地址相同设定。也就是说本特性在 iframe 中不允许跨域访问。
'none': 本特性默认在最上层和包含的内容中(iframes)都禁止。
'src': (只在 iframe 中允许) 只要在 src 中的 URL 和加载 iframe 用的 URL 相同,则本特性在 iframe 中允许。
其中*或'none'只允许单独使用,而'self'和'src'可以与多个源地址一起使用。
语法
禁止全屏模式
可以设置 fullscreen 的特征策略为'none'。
<iframe allow="fullscreen 'none'">
复制代码
支持多个特征策略
禁止全屏模式和禁止调起支付接口,多个用分号隔开。
<iframe allow="fullscreen 'none'; payment 'none'">
复制代码
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,然而该页面企图从自己的源 (http://example.com) 加载。
height
以 CSS 像素格式 HTML5,或像素格式 HTML 4.01,或百分比格式指定 frame 的高度。默认值为 150。
importance
表示 <iframe>
的 src 属性指定的资源的加载优先级。允许的值有:
auto (default):不指定优先级。浏览器根据自身情况决定资源的加载顺序
high:资源的加载优先级较高
low:资源的加载优先级较低
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://www.infoq.cn/" target="myIframe">掘金</a>
复制代码
在 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://www.infoq.cn/', window.name);
};
};
</script>
</body>
</html>
复制代码
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://www.infoq.cn/" width="500" height="200" />
复制代码
no-referrer
将 referrerpolicy 的值设置为 no-referrer
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200" referrerpolicy="no-referrer" />
复制代码
origin
将 referrerpolicy 的值设置为 origin
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200" referrerpolicy="origin" />
复制代码
origin-when-cross-origin
将 referrerpolicy 的值设置为 origin-when-cross-origin
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200" referrerpolicy="origin-when-cross-origin" />
复制代码
same-origin
将 referrerpolicy 的值设置为 same-origin
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200" referrerpolicy="same-origin" />
复制代码
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 的来源中有完整的路径:
unsafe-url
将 referrerpolicy 的值设置为 unsafe-url
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200" referrerpolicy="unsafe-url" />
复制代码
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: 允许嵌入的浏览上下文在经过用户允许后导航(加载)内容到顶级的浏览上下文。
src
被嵌套的页面的 URL 地址。使用 about:blank
值可以嵌入一个遵从同源策略的空白页。在 Firefox (version 65 及更高版本)、基于 Chromium 的浏览器、Safari/iOS 中使用代码移除 iframe 的 src 属性(例如通过 Element.removeAttribute() )会导致 about:blank
被载入 frame。
<iframe name="myIframe" src="https://www.infoq.cn/" width="500" height="200"></iframe>
复制代码
srcdoc HTML5 only
该属性是一段 HTML 代码,这些代码会被渲染到 iframe 中。如果浏览器不支持 srcdoc 属性,则会渲染 src 属性表示的内容。
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 来实现。
脚本通过 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
注:通过 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/,即可访问本地页面:
框架内部脚本可以通过 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
定位和缩放
作为一个可替换元素, 可以使用 object-position 和 object-fit 来定位、对齐、缩放 <iframe> 元素内的文档。
iframe 跨域通信
前面把 iframe 基本的知识点都过了一遍,加深了对 iframe 的了解。那么我们开头所讲的实际业务需求中遇到的问题,iframe 的跨域通信来了。
讲跨域通信之前,我们先了解一下什么是跨域,以及常见的跨域场景都有哪些。
跨域
同源策略
同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
如果两个 URL 的协议、主机、端口元组都相同的话,则这两个 URL 是同源。
如果两个 URL 的协议、主机、端口元组任意一个不同时,这两个 URL 不同源,不同源之间相互请求资源,称之为跨域通信。
跨域场景
下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:
iframe 跨域通信解决方案
我们把内嵌 iframe 的页面称之为父页面,被嵌套页面为子页面,两个页面间需要传递数据,进而达成我们需要的功能,不同方向的通信,实现方式也不一样。
父页面向子页面传递数据
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
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
子页面向父页面传递数据
子页面通过也可以通过 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
总结
温故而知新,可以为师矣。古人诚不欺我。
通过再次学习 iframe,对 iframe 了解加深,且对于之前时常记忆不深的跨域解决方案有了更深刻的记忆。
时不我待,感谢不断坚持的自己,加油!
评论