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>标签主机单独的嵌入式窗口,有自己的独立documentwindow对象。

我们可以使用属性访问它们:

  • 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.competer.site.comsite.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和一种形式。

请注意,没有任何效果。因此,默认设置确实很苛刻:

结果 index.html sandboxed.html   请注意:

"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(...)
});

完整的例子:

结果 iframe.html index.html  

概要

要调用方法并访问另一个窗口的内容,我们应该首先对其进行引用。

对于弹出窗口,我们有以下参考:

  • 在打开器窗口中:window.open–打开一个新窗口并返回对该窗口的引用,
  • 从弹出窗口:window.opener–是对弹出窗口中打开器窗口的引用。

对于iframe,我们可以使用以下方法访问父/子窗口:

  • window.frames –嵌套窗口对象的集合,
  • window.parentwindow.top是对父窗口和顶部窗口的引用,
  • iframe.contentWindow<iframe>标签内的窗口。

如果Windows共享相同的来源(主机,端口,协议),则Windows可以相互执行任何操作。

否则,只有可能的操作是:

  • 更改location另一个窗口的(只读访问)。
  • 向其发布消息。

例外是:

  • 共享相同二级域的Windows:a.site.comb.site.com。然后document.domain='site.com'将它们都设置为“原点相同”状态。
  • 如果iframe具有sandbox属性,则除非allow-same-origin该属性值中指定,否则它将被强制置于“其他来源”状态。这可用于在同一站点的iframe中运行不受信任的代码。

postMessage界面允许两个具有任何起源的窗口进行交谈:

  1. 发件人呼叫targetWin.postMessage(data, targetOrigin)

  2. 如果targetOrigin不是'*',则浏览器检查window targetWin是否具有原点targetOrigin

  3. 如果是这样,则使用特殊属性targetWin触发message事件:

    • origin–发送者窗口的来源(如http://my.site.com
    • source –对发件人窗口的引用。
    • data –数据,除IE仅支持字符串的任何地方的任何对象。

    我们应该使用addEventListener它在目标窗口内设置此事件的处理程序。

上一篇:张飞硬件90天学习笔记和相关文档汇总


下一篇:HTML5 之跨域通讯(postMessage)