JS-iframe-跨窗口通讯
备注:补充一个额外方案:建立一个中间页面,使子窗口跳转到和主窗口同域的页面中,再使用子窗口的parent操作父窗口。
跨窗口通讯
“相同来源”(相同站点)策略限制了窗口和框架之间的访问。
这样的想法是,如果用户打开了两个页面:一个来自john-smith.com
,而另一个打开gmail.com
,则他们将不需要脚本来john-smith.com
从中读取我们的邮件gmail.com
。因此,“相同来源”策略的目的是保护用户免遭信息盗窃。
同源
如果两个URL具有相同的协议,域和端口,则称它们具有“相同的来源”。
这些URL都具有相同的来源:
http://site.com
http://site.com/
http://site.com/my/page.html
这些没有:
http://www.site.com
(另一个领域:www.
重要)http://site.org
(另一个领域:.org
重要)https://site.com
(另一种协议:https
)http://site.com:8080
(另一个端口:8080
)
“相同来源”政策规定:
- 如果我们引用了另一个窗口,例如由创建的弹出
window.open
窗口或内部的一个窗口<iframe>
,并且该窗口来自同一原点,则可以完全访问该窗口。 - 否则,如果它来自另一个来源,那么我们将无法访问该窗口的内容:变量,文档等。唯一的例外是
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
与 iframe.contentWindow.onload
的iframe.onload
(在事件<iframe>
标签)是基本上相同iframe.contentWindow.onload
(在嵌入窗口对象)。当嵌入式窗口完全加载所有资源时触发。
…但是我们无法iframe.contentWindow.onload
从其他来源访问iframe,因此请使用iframe.onload
。
Windows在子域上:document.domain
根据定义,两个具有不同域的URL具有不同的来源。
但是,如果窗口共享同一个二级域名,例如john.site.com
,peter.site.com
和site.com
(使他们共同的二级域名site.com
),我们可以让浏览器忽略这种差别,使他们能够从“同根同源”来对待为了跨窗口交流的目的。
为了使其正常工作,每个这样的窗口都应运行以下代码:
document.domain = 'site.com';
就这样。现在,他们可以不受限制地进行交互。同样,这仅适用于具有相同二级域的页面。
iframe:错误的文档陷阱
当iframe来自同一来源时,我们可能会访问它 document
,这是一个陷阱。它与跨源事物无关,但重要的是要知道。
创建后,iframe会立即拥有一个文档。但是该文档不同于加载到其中的文档!
因此,如果我们立即对文档进行操作,则可能会丢失。
在这里,看看:
<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的文档,因为那是错误的文档。如果我们在其上设置了任何事件处理程序,它们将被忽略。
如何检测文档在那里的时刻?
iframe.onload
触发时,正确的文档肯定存在。但是,只有在加载了所有资源的整个iframe时,它才会触发。
我们可以尝试使用check in来赶上这一时刻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>
in 内部的某些操作,以防止其执行不受信任的代码。它通过将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
- 允许从
window.open
弹出窗口iframe
有关更多信息,请参见手册。
以下示例演示了具有默认限制集的沙盒iframe <iframe sandbox src="...">
。它具有一些JavaScript和一种形式。
请注意,没有任何效果。因此,默认设置确实很苛刻:
该"sandbox"
属性的目的只是添加更多限制。它无法删除它们。特别是,如果iframe来自其他来源,则无法放宽同源限制。
跨窗口消息传递
该postMessage
界面允许Windows彼此交谈,无论它们来自哪个来源。
因此,这是解决“相同来源”政策的一种方法。它允许从窗口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>
消息
要接收消息,目标窗口应具有message
事件处理程序。触发何时postMessage
调用(并targetOrigin
检查成功)。
事件对象具有特殊的属性:
data
- 来自的数据
postMessage
。 origin
- 例如,发件人的来源
http://javascript.info
。 source
- 对发件人窗口的引用。
source.postMessage(...)
如果需要,我们可以立即返回。
要分配该处理程序,我们应该使用addEventListener
,短语法window.onmessage
不起作用。
这是一个例子:
window.addEventListener("message", function(event) {
if (event.origin != 'http://javascript.info') {
// something from an unknown domain, let's ignore it
return;
}
alert( "received: " + event.data );
// can message back using event.source.postMessage(...)
});
完整的例子:
概要
要调用方法并访问另一个窗口的内容,我们应该首先对其进行引用。
对于弹出窗口,我们有以下参考:
- 在打开器窗口中:
window.open
–打开一个新窗口并返回对该窗口的引用, - 从弹出窗口:
window.opener
–是对弹出窗口中打开器窗口的引用。
对于iframe,我们可以使用以下方法访问父/子窗口:
window.frames
–嵌套窗口对象的集合,window.parent
,window.top
是对父窗口和顶部窗口的引用,iframe.contentWindow
是<iframe>
标签内的窗口。
如果Windows共享相同的来源(主机,端口,协议),则Windows可以相互执行任何操作。
否则,只有可能的操作是:
- 更改
location
另一个窗口的(只读访问)。 - 向其发布消息。
例外是:
- 共享相同二级域的Windows:
a.site.com
和b.site.com
。然后document.domain='site.com'
将它们都设置为“原点相同”状态。 - 如果iframe具有
sandbox
属性,则除非allow-same-origin
该属性值中指定,否则它将被强制置于“其他来源”状态。这可用于在同一站点的iframe中运行不受信任的代码。
该postMessage
界面允许两个具有任何起源的窗口进行交谈:
发件人呼叫
targetWin.postMessage(data, targetOrigin)
。如果
targetOrigin
不是'*'
,则浏览器检查windowtargetWin
是否具有原点targetOrigin
。如果是这样,则使用特殊属性
targetWin
触发message
事件:origin
–发送者窗口的来源(如http://my.site.com
)source
–对发件人窗口的引用。data
–数据,除IE仅支持字符串的任何地方的任何对象。
我们应该使用
addEventListener
它在目标窗口内设置此事件的处理程序。