Javascript 跨窗口通信

2023-02-17 10:56 更新

“同源(Same Origin)”策略限制了窗口(window)和 frame 之間的相互訪問。

這個想法出于這樣的考慮,如果一個用戶有兩個打開的頁面:一個來自 john-smith.com,另一個是 gmail.com,那么用戶將不希望 john-smith.com 的腳本可以讀取 gmail.com 中的郵件。所以,“同源”策略的目的是保護(hù)用戶免遭信息盜竊。

同源

如果兩個 URL 具有相同的協(xié)議,域和端口,則稱它們是“同源”的。

以下的幾個 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?(另一個協(xié)議:?https?)
  • ?http://site.com:8080?(另一個端口:?8080?)

“同源”策略規(guī)定:

  • 如果我們有對另外一個窗口(例如,一個使用 ?window.open? 創(chuàng)建的彈窗,或者一個窗口中的 iframe)的引用,并且該窗口是同源的,那么我們就具有對該窗口的全部訪問權(quán)限。
  • 否則,如果該窗口不是同源的,那么我們就無法訪問該窗口中的內(nèi)容:變量,文檔,任何東西。唯一的例外是 ?location?:我們可以修改它(進(jìn)而重定向用戶)。但是我們無法讀取 ?location?(因此,我們無法看到用戶當(dāng)前所處的位置,也就不會泄漏任何信息)。

實(shí)例:iframe

一個 <iframe> 標(biāo)簽承載了一個單獨(dú)的嵌入的窗口,它具有自己的 document 和 window

我們可以使用以下屬性訪問它們:

  • ?iframe.contentWindow? 來獲取 ?<iframe>? 中的 window。
  • ?iframe.contentDocument? 來獲取 ?<iframe>? 中的 document,是 ?iframe.contentWindow.document? 的簡寫形式。

當(dāng)我們訪問嵌入的窗口中的東西時,瀏覽器會檢查 iframe 是否具有相同的源。如果不是,則會拒絕訪問(對 location 進(jìn)行寫入是一個例外,它是會被允許的)。

例如,讓我們嘗試對來自另一個源的 <iframe> 進(jìn)行讀取和寫入:

<iframe src="https://example.com" rel="external nofollow"  id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // 我們可以獲取對內(nèi)部 window 的引用
    let iframeWindow = iframe.contentWindow; // OK
    try {
      // ...但是無法獲取其中的文檔
      let doc = iframe.contentDocument; // ERROR
    } catch(e) {
      alert(e); // Security Error(另一個源)
    }

    // 并且,我們也無法讀取 iframe 中頁面的 URL
    try {
      // 無法從 location 對象中讀取 URL
      let href = iframe.contentWindow.location.href; // ERROR
    } catch(e) {
      alert(e); // Security Error
    }

    // ...我們可以寫入 location(所以,在 iframe 中加載了其他內(nèi)容)!
    iframe.contentWindow.location = '/'; // OK

    iframe.onload = null; // 清空處理程序,在 location 更改后不要再運(yùn)行它
  };
</script>

上述代碼除了以下操作都會報錯:

  • 通過 ?iframe.contentWindow? 獲取對內(nèi)部 window 的引用 —— 這是被允許的。
  • 對 ?location? 進(jìn)行寫入

與此相反,如果 <iframe> 具有相同的源,我們可以使用它做任何事情:

<!-- 來自同一個網(wǎng)站的 iframe -->
<iframe src="/" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // 可以做任何事兒
    iframe.contentDocument.body.prepend("Hello, world!");
  };
</script>

?iframe.onload? vs ?iframe.contentWindow.onload?

iframe.onload 事件(在 <iframe> 標(biāo)簽上)與 iframe.contentWindow.onload(在嵌入的 window 對象上)基本相同。當(dāng)嵌入的窗口的所有資源都完全加載完畢時觸發(fā)。

……但是,我們無法使用 iframe.contentWindow.onload 訪問不同源的 iframe。因此,請使用 iframe.onload

子域上的 window:document.domain

根據(jù)定義,兩個具有不同域的 URL 具有不同的源。

但是,如果窗口的二級域相同,例如 john.site.com,peter.site.com 和 site.com(它們共同的二級域是 site.com),我們可以使瀏覽器忽略該差異,使得它們可以被作為“同源”的來對待,以便進(jìn)行跨窗口通信。

為了做到這一點(diǎn),每個這樣的窗口都應(yīng)該執(zhí)行下面這行代碼:

document.domain = 'site.com';

這樣就可以了。現(xiàn)在它們可以無限制地進(jìn)行交互了。但是再強(qiáng)調(diào)一遍,這僅適用于具有相同二級域的頁面。

已棄用,但仍有效

document.domain 屬性正在被從 規(guī)范 中刪除。跨窗口通信(下面將很快解釋到)是建議的替代方案。

也就是說,到目前為止,所有瀏覽器都支持它。并且未來也將繼續(xù)支持它,而不會導(dǎo)致使用了 document.domain 的舊代碼出現(xiàn)問題。

Iframe:錯誤文檔陷阱

當(dāng)一個 iframe 來自同一個源時,我們可能會訪問其 document,但是這里有一個陷阱。它與跨源無關(guān),但你一定要知道。

在創(chuàng)建 iframe 后,iframe 會立即就擁有了一個文檔。但是該文檔不同于加載到其中的文檔!

因此,如果我們要立即對文檔進(jìn)行操作,就可能出問題。

看一下下面這段代碼:

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;
  iframe.onload = function() {
    let newDoc = iframe.contentDocument;
    // 加載的文檔與初始的文檔不同!
    alert(oldDoc == newDoc); // false
  };
</script>

我們不應(yīng)該對尚未加載完成的 iframe 的文檔進(jìn)行處理,因?yàn)槟鞘?nbsp;錯誤的文檔。如果我們在其上設(shè)置了任何事件處理程序,它們將會被忽略。

如何檢測文檔就位(加載完成)的時刻呢?

正確的文檔在 iframe.onload 觸發(fā)時肯定就位了。但是,只有在整個 iframe 和它所有資源都加載完成時,iframe.onload 才會觸發(fā)。

我們可以嘗試通過在 setInterval 中進(jìn)行檢查,以更早地捕獲該時刻:

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;

  // 每 100ms 檢查一次文檔是否為新文檔
  let timer = setInterval(() => {
    let newDoc = iframe.contentDocument;
    if (newDoc == oldDoc) return;

    alert("New document is here!");

    clearInterval(timer); // 取消 setInterval,不再需要它做任何事兒
  }, 100);
</script>

集合:window.frames

獲取 <iframe> 的 window 對象的另一個方式是從命名集合 window.frames 中獲?。?

  • 通過索引獲取:?window.frames[0]? —— 文檔中的第一個 iframe 的 window 對象。
  • 通過名稱獲?。?window.frames.iframeName? —— 獲取 ?name="iframeName"? 的 iframe 的 window 對象。

例如:

<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 內(nèi)可能嵌套了其他的 iframe。相應(yīng)的 window 對象會形成一個層次結(jié)構(gòu)(hierarchy)。

可以通過以下方式獲取:

  • ?window.frames? —— “子”窗口的集合(用于嵌套的 iframe)。
  • ?window.parent? —— 對“父”(外部)窗口的引用。
  • ?window.top? —— 對最頂級父窗口的引用。

例如:

window.frames[0].parent === window; // true

我們可以使用 top 屬性來檢查當(dāng)前的文檔是否是在 iframe 內(nèi)打開的:

if (window == top) { // 當(dāng)前 window == window.top?
  alert('The script is in the topmost window, not in a frame');
} else {
  alert('The script runs in a frame!');
}

“sandbox” iframe 特性

sandbox 特性(attribute)允許在 <iframe> 中禁止某些特定行為,以防止其執(zhí)行不被信任的代碼。它通過將 iframe 視為非同源的,或者應(yīng)用其他限制來實(shí)現(xiàn) iframe 的“沙盒化”。

對于 <iframe sandbox src="...">,有一個應(yīng)用于其上的默認(rèn)的限制集。但是,我們可以通過提供一個以空格分隔的限制列表作為特性的值,來放寬這些限制,該列表中的各項(xiàng)為不應(yīng)該應(yīng)用于這個 iframe 的限制,例如:<iframe sandbox="allow-forms allow-popups">。

換句話說,一個空的 "sandbox" 特性會施加最嚴(yán)格的限制,但是我們用一個以空格分隔的列表,列出要移除的限制。

以下是限制的列表:

?allow-same-origin ?

默認(rèn)情況下,?"sandbox"? 會為 iframe 強(qiáng)制實(shí)施“不同來源”的策略。換句話說,它使瀏覽器將 ?iframe? 視為來自另一個源,即使其 ?src? 指向的是同一個網(wǎng)站也是如此。具有所有隱含的腳本限制。此選項(xiàng)會移除這些限制。

?allow-top-navigation ?

允許 ?iframe? 更改 ?parent.location?。

?allow-forms ?

允許在 ?iframe? 中提交表單。

?allow-scripts ?

允許在 ?iframe? 中運(yùn)行腳本。

?allow-popups ?

允許在 ?iframe? 中使用 ?window.open? 打開彈窗。

查看 官方手冊 獲取更多內(nèi)容。

下面的示例演示了一個具有默認(rèn)限制集的沙盒 iframe:<iframe sandbox src="...">。它有一些 JavaScript 代碼和一個表單。

請注意,這里沒有東西會運(yùn)行。可見默認(rèn)設(shè)置非??量蹋?

示例代碼

請注意:

"sandbox" 特性的目的僅是 添加更多 限制。它無法移除這些限制。尤其是,如果 iframe 來自其他源,則無法放寬同源策略。

跨窗口通信

?postMessage? 接口允許窗口之間相互通信,無論它們來自什么源。

因此,這是解決“同源”策略的方式之一。它允許來自于 john-smith.com 的窗口與來自于 gmail.com 的窗口進(jìn)行通信,并交換信息,但前提是它們雙方必須均同意并調(diào)用相應(yīng)的 JavaScript 函數(shù)。這可以保護(hù)用戶的安全。

這個接口有兩個部分。

postMessage

想要發(fā)送消息的窗口需要調(diào)用接收窗口的 postMessage 方法。換句話說,如果我們想把消息發(fā)送給 win,我們應(yīng)該調(diào)用 win.postMessage(data, targetOrigin)。

參數(shù):

?data ?

要發(fā)送的數(shù)據(jù)??梢允侨魏螌ο螅瑪?shù)據(jù)會被通過使用“結(jié)構(gòu)化序列化算法(structured serialization algorithm)”進(jìn)行克隆。IE 瀏覽器只支持字符串,因此我們需要對復(fù)雜的對象調(diào)用 ?JSON.stringify? 方法進(jìn)行處理,以支持該瀏覽器。

?targetOrigin ?

指定目標(biāo)窗口的源,以便只有來自給定的源的窗口才能獲得該消息。

targetOrigin 是一種安全措施。請記住,如果目標(biāo)窗口是非同源的,我們無法在發(fā)送方窗口讀取它的 location。因此,我們無法確定當(dāng)前在預(yù)期的窗口中打開的是哪個網(wǎng)站:用戶隨時可以導(dǎo)航離開,并且發(fā)送方窗口對此一無所知。

指定 targetOrigin 可以確保窗口僅在當(dāng)前仍處于正確的網(wǎng)站時接收數(shù)據(jù)。在有敏感數(shù)據(jù)時,這非常重要。

例如,這里的 win 僅在它擁有來自 http://example.com 這個源的文檔時,才會接收消息:

<iframe src="http://example.com" rel="external nofollow"  rel="external nofollow"  name="example">

<script>
  let win = window.frames.example;

  win.postMessage("message", "http://example.com");
</script>

如果我們不希望做這個檢查,可以將 targetOrigin 設(shè)置為 *。

<iframe src="http://example.com" rel="external nofollow"  rel="external nofollow"  name="example">

<script>
  let win = window.frames.example;

  win.postMessage("message", "*");
</script>

onmessage

為了接收消息,目標(biāo)窗口應(yīng)該在 message 事件上有一個處理程序。當(dāng) postMessage 被調(diào)用時觸發(fā)該事件(并且 targetOrigin 檢查成功)。

event 對象具有特殊屬性:

?data ?

從 ?postMessage? 傳遞來的數(shù)據(jù)。

?origin ?

發(fā)送方的源,例如 ?http://javascript.info?。

?source ?

對發(fā)送方窗口的引用。如果我們想,我們可以立即 ?source.postMessage(...)? 回去。

要為 message 事件分配處理程序,我們應(yīng)該使用 addEventListener,簡短的語法 window.onmessage 不起作用。

這里有一個例子:

window.addEventListener("message", function(event) {
  if (event.origin != 'http://javascript.info') {
    // 來自未知的源的內(nèi)容,我們忽略它
    return;
  }

  alert( "received: " + event.data );

  // 可以使用 event.source.postMessage(...) 向回發(fā)送消息
});

完整示例

總結(jié)

要調(diào)用另一個窗口的方法或者訪問另一個窗口的內(nèi)容,我們應(yīng)該首先擁有對其的引用。

對于彈窗,我們有兩個引用:

  • 從打開窗口的(opener)窗口:?window.open? —— 打開一個新的窗口,并返回對它的引用,
  • 從彈窗:?window.opener? —— 是從彈窗中對打開此彈窗的窗口(opener)的引用。

對于 iframe,我們可以使用以下方式訪問父/子窗口:

  • ?window.frames? —— 一個嵌套的 window 對象的集合,
  • ?window.parent?,?window.top? 是對父窗口和頂級窗口的引用,
  • ?iframe.contentWindow? 是 ?<iframe>? 標(biāo)簽內(nèi)的 window 對象。

如果幾個窗口的源相同(域,端口,協(xié)議),那么這幾個窗口可以彼此進(jìn)行所需的操作。

否則,只能進(jìn)行以下操作:

  • 更改另一個窗口的 ?location?(只能寫入)。
  • 向其發(fā)送一條消息。

例外情況:

  • 對于二級域相同的窗口:?a.site.com? 和 ?b.site.com?。通過在這些窗口中均設(shè)置 ?document.domain='site.com'?,可以使它們處于“同源”狀態(tài)。
  • 如果一個 iframe 具有 ?sandbox? 特性(attribute),則它會被強(qiáng)制處于“非同源”狀態(tài),除非在其特性值中指定了 ?allow-same-origin?。這可用于在同一網(wǎng)站的 iframe 中運(yùn)行不受信任的代碼。

postMessage 接口允許兩個具有任何源的窗口之間進(jìn)行通信:

  1. 發(fā)送方調(diào)用 ?targetWin.postMessage(data, targetOrigin)?。
  2. 如果 ?targetOrigin? 不是 ?'*'?,那么瀏覽器會檢查窗口 ?targetWin? 是否具有源 ?targetOrigin?。
  3. 如果它具有,?targetWin? 會觸發(fā)具有特殊的屬性的 ?message? 事件:
    • ?origin? —— 發(fā)送方窗口的源(比如 ?http://my.site.com?)。
    • ?source? —— 對發(fā)送方窗口的引用。
    • ?data? —— 數(shù)據(jù),可以是任何對象。但是 IE 瀏覽器只支持字符串,因此我們需要對復(fù)雜的對象調(diào)用 ?JSON.stringify? 方法進(jìn)行處理,以支持該瀏覽器。

    我們應(yīng)該使用 addEventListener 來在目標(biāo)窗口中設(shè)置 message 事件的處理程序。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號