“相同来源”(同一站点)策略限制了窗口和框架之间的相互访问。
其理念是,如果用户打开了两个页面:一个来自john-smith.com
,另一个来自gmail.com
,那么他们不希望来自john-smith.com
的脚本读取来自gmail.com
的邮件。因此,“相同来源”策略的目的是保护用户免受信息盗窃。
相同来源
如果两个 URL 具有相同的协议、域名和端口,则它们被称为具有“相同来源”。
以下 URL 具有相同的来源
http://site.com
http://site.com/
http://site.com/my/page.html
以下 URL 不具有相同的来源
http://www.site.com
(另一个域名:www.
不同)http://site.org
(另一个域名:.org
不同)https://site.com
(另一个协议:https
)http://site.com:8080
(另一个端口:8080
)
“相同来源”策略规定:
- 如果我们有一个对另一个窗口的引用,例如由
window.open
创建的弹出窗口或<iframe>
内部的窗口,并且该窗口来自相同的来源,那么我们可以完全访问该窗口。 - 否则,如果它来自另一个来源,那么我们无法访问该窗口的内容:变量、文档、任何内容。唯一的例外是
location
:我们可以更改它(从而重定向用户)。但我们无法读取 location(因此我们无法看到用户现在的位置,没有信息泄露)。
实际应用:iframe
<iframe>
标签承载一个独立的嵌入式窗口,具有其自己的独立 document
和 window
对象。
我们可以使用属性访问它们
iframe.contentWindow
获取<iframe>
内部的窗口。iframe.contentDocument
获取<iframe>
内部的文档,是iframe.contentWindow.document
的简写。
当我们访问嵌入式窗口内部的内容时,浏览器会检查 iframe 是否具有相同的来源。如果不是,则访问被拒绝(写入 location
是一个例外,它仍然允许)。
例如,让我们尝试从另一个来源读取和写入 <iframe>
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// we can get the reference to the inner window
let iframeWindow = iframe.contentWindow; // OK
try {
// ...but not to the document inside it
let doc = iframe.contentDocument; // ERROR
} catch(e) {
alert(e); // Security Error (another origin)
}
// also we can't READ the URL of the page in iframe
try {
// Can't read URL from the Location object
let href = iframe.contentWindow.location.href; // ERROR
} catch(e) {
alert(e); // Security Error
}
// ...we can WRITE into location (and thus load something else into the iframe)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // clear the handler, not to run it after the location change
};
</script>
上面的代码显示了除以下操作之外的任何操作的错误
- 获取对内部窗口
iframe.contentWindow
的引用 - 这是允许的。 - 写入
location
。
相反,如果 <iframe>
具有相同的来源,我们可以对其进行任何操作
<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// just do anything
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
iframe.onload
vs iframe.contentWindow.onload
iframe.onload
事件(在 <iframe>
标签上)本质上与 iframe.contentWindow.onload
(在嵌入式窗口对象上)相同。它在嵌入式窗口完全加载所有资源时触发。
…但我们无法访问来自不同来源的 iframe 的 iframe.contentWindow.onload
,因此使用 iframe.onload
。
子域上的窗口:document.domain
根据定义,两个具有不同域的 URL 具有不同的来源。
但如果窗口共享相同的二级域,例如 john.site.com
、peter.site.com
和 site.com
(因此它们的公共二级域为 site.com
),我们可以让浏览器忽略该差异,以便它们可以被视为来自“相同来源”的,用于跨窗口通信。
要使其正常工作,每个这样的窗口都应运行以下代码
document.domain = 'site.com';
就是这样。现在它们可以不受限制地交互。同样,这仅适用于具有相同二级域的页面。
document.domain
属性正在从 规范 中删除。跨窗口消息传递(将在下面解释)是建议的替代方案。
也就是说,截至目前,所有浏览器都支持它。并且支持将保留在未来,以避免破坏依赖于 document.domain
的旧代码。
Iframe:错误的文档陷阱
当 iframe 来自同一来源时,我们可以访问它的 document
,存在一个陷阱。它与跨源问题无关,但很重要。
iframe 创建后立即拥有一个 document。但该 document 与加载到其中的 document 不同!
因此,如果我们立即对 document 做一些操作,它可能会丢失。
这里,看看
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// the loaded document is not the same as initial!
alert(oldDoc == newDoc); // false
};
</script>
我们不应该使用尚未加载的 iframe 的 document,因为那是错误的 document。如果我们在其上设置任何事件处理程序,它们将被忽略。
如何检测 document 存在的那一刻?
当 iframe.onload
触发时,正确的 document 一定已到位。但它只在整个 iframe 及其所有资源加载完毕时触发。
我们可以尝试使用 setInterval
中的检查来更早地捕获那一刻
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// every 100 ms check if the document is the new one
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("New document is here!");
clearInterval(timer); // cancel setInterval, don't need it any more
}, 100);
</script>
集合:window.frames
获取 <iframe>
的窗口对象的另一种方法是 - 从名为 window.frames
的集合中获取它
- 按编号:
window.frames[0]
- 文档中第一个框架的窗口对象。 - 按名称:
window.frames.iframeName
- 具有name="iframeName"
的框架的窗口对象。
例如
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
一个 iframe 可以包含其他 iframe。相应的 window
对象形成一个层次结构。
导航链接是
window.frames
– “子” 窗口的集合(用于嵌套框架)。window.parent
– 对“父” (外部) 窗口的引用。window.top
– 对最顶层父窗口的引用。
例如
window.frames[0].parent === window; // true
我们可以使用 top
属性来检查当前文档是否在框架内打开。
if (window == top) { // current window == window.top?
alert('The script is in the topmost window, not in a frame');
} else {
alert('The script runs in a frame!');
}
“沙箱” iframe 属性
sandbox
属性允许排除 <iframe>
内的某些操作,以防止执行不受信任的代码。它通过将 iframe 视为来自另一个来源和/或应用其他限制来“沙箱化” iframe。
对于 <iframe sandbox src="...">
,会应用一组“默认”限制。但如果我们提供一个以空格分隔的限制列表作为属性的值,则可以放宽这些限制,例如:<iframe sandbox="allow-forms allow-popups">
。
换句话说,一个空的 "sandbox"
属性会施加最严格的限制,但我们可以放置一个空格分隔的列表,列出我们想要解除的限制。
以下是限制列表
allow-same-origin
- 默认情况下,
"sandbox"
会对 iframe 强制执行“不同来源”策略。换句话说,它会让浏览器将iframe
视为来自另一个来源,即使它的src
指向同一个站点。对于脚本,会施加所有隐含的限制。此选项会移除该功能。 allow-top-navigation
- 允许
iframe
更改parent.location
。 allow-forms
- 允许从
iframe
提交表单。 allow-scripts
- 允许从
iframe
运行脚本。 allow-popups
- 允许从
iframe
window.open
弹出窗口。
有关更多信息,请参阅 手册。
以下示例演示了一个带有默认限制集的沙箱化 iframe:<iframe sandbox src="...">
。它包含一些 JavaScript 和一个表单。
请注意,没有任何内容有效。因此,默认集确实很严格。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div>The iframe below has the <code>sandbox</code> attribute.</div>
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button onclick="alert(123)">Click to run a script (doesn't work)</button>
<form action="http://google.com">
<input type="text">
<input type="submit" value="Submit (doesn't work)">
</form>
</body>
</html>
"sandbox"
属性的用途仅仅是添加更多限制。它不能移除限制。特别是,如果 iframe 来自另一个来源,它不能放宽同源限制。
跨窗口消息传递
postMessage
接口允许窗口之间相互通信,无论它们来自哪个来源。
因此,它是绕过“同源策略”的一种方法。它允许来自 john-smith.com
的窗口与 gmail.com
通信并交换信息,但前提是它们都同意并调用相应的 JavaScript 函数。这对于用户来说是安全的。
该接口有两个部分。
postMessage
想要发送消息的窗口会调用接收窗口的 postMessage 方法。换句话说,如果我们要向 win
发送消息,我们应该调用 win.postMessage(data, targetOrigin)
。
参数
data
- 要发送的数据。可以是任何对象,数据使用“结构化序列化算法”进行克隆。IE 只支持字符串,因此我们应该
JSON.stringify
复杂对象以支持该浏览器。 targetOrigin
- 指定目标窗口的来源,以便只有来自给定来源的窗口才能收到消息。
targetOrigin
是一种安全措施。请记住,如果目标窗口来自另一个来源,我们无法在发送窗口中读取其 location
。因此,我们无法确定目标窗口中当前打开了哪个网站:用户可能会导航离开,而发送窗口对此一无所知。
指定 targetOrigin
可确保窗口仅在它仍处于正确网站时才接收数据。当数据敏感时,这一点很重要。
例如,这里 win
只有在它拥有来自 http://example.com
来源的文档时才会收到消息。
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
如果我们不想进行该检查,我们可以将 targetOrigin
设置为 *
。
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
onmessage
要接收消息,目标窗口应该在 message
事件上有一个处理程序。当调用 postMessage
(并且 targetOrigin
检查成功)时,它会触发。
事件对象具有特殊属性
data
- 来自
postMessage
的数据。 origin
- 发送者的来源,例如
https://javascript.js.cn
。 source
- 对发送者窗口的引用。如果需要,我们可以立即
source.postMessage(...)
回复。
要分配该处理程序,我们应该使用 addEventListener
,简短语法 window.onmessage
不起作用。
以下是一个示例
window.addEventListener("message", function(event) {
if (event.origin != 'https://javascript.js.cn') {
// something from an unknown domain, let's ignore it
return;
}
alert( "received: " + event.data );
// can message back using event.source.postMessage(...)
});
完整示例
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Receiving iframe.
<script>
window.addEventListener('message', function(event) {
alert(`Received ${event.data} from ${event.origin}`);
});
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form id="form">
<input type="text" placeholder="Enter message" name="message">
<input type="submit" value="Click to send">
</form>
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
<script>
form.onsubmit = function() {
iframe.contentWindow.postMessage(this.message.value, '*');
return false;
};
</script>
</body>
</html>
摘要
要调用方法并访问另一个窗口的内容,我们首先需要一个对它的引用。
对于弹出窗口,我们有以下引用:
- 从打开窗口:
window.open
- 打开一个新窗口并返回对它的引用, - 从弹出窗口:
window.opener
- 是从弹出窗口到打开窗口的引用。
对于 iframe,我们可以使用以下方法访问父/子窗口:
window.frames
- 一组嵌套的窗口对象,window.parent
,window.top
是对父窗口和顶层窗口的引用,iframe.contentWindow
是<iframe>
标签内部的窗口。
如果窗口共享相同的来源(主机、端口、协议),那么窗口可以相互执行任何操作。
否则,只有以下操作是可能的:
- 更改另一个窗口的
location
(只写访问)。 - 向它发送消息。
例外情况是:
- 共享相同二级域的窗口:
a.site.com
和b.site.com
。然后在两个窗口中设置document.domain='site.com'
将它们置于“相同来源”状态。 - 如果 iframe 具有
sandbox
属性,则它将强制置于“不同来源”状态,除非在属性值中指定了allow-same-origin
。这可以用来从同一个站点运行 iframe 中不受信任的代码。
postMessage
接口允许具有任何来源的两个窗口进行通信
-
发送方调用
targetWin.postMessage(data, targetOrigin)
。 -
如果
targetOrigin
不是'*'
,则浏览器会检查窗口targetWin
是否具有来源targetOrigin
。 -
如果是,则
targetWin
会触发message
事件,并带有以下特殊属性:origin
- 发送方窗口的来源(如http://my.site.com
)source
- 对发送方窗口的引用。data
- 数据,除了 IE 之外的任何对象,IE 仅支持字符串。
我们应该使用
addEventListener
在目标窗口中设置此事件的处理程序。
评论
<code>
标签,对于多行代码,请用<pre>
标签包裹,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)