【前端知識點】跨域之跨域window.postMessage
window.postMessage() 方法可以安全地實現跨源通訊。通常,對於兩個不同頁面的指令碼,只有當執行它們的頁面位於具有相同的協議(通常為https),埠號(443為https的預設值),以及主機 (兩個頁面的模數 Document.domain
設定為相同的值)
時,這兩個指令碼才能相互通訊。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。
window.postMessage() 方法被呼叫時,會在所有頁面指令碼執行完畢之後(e.g., 在該方法之後設定的事件、之前設定的timeout 事件,etc.)向目標視窗派發一個 MessageEvent
訊息。
該MessageEvent
訊息有四個屬性需要注意:
message 屬性表示該message 的型別; data 屬性為 window.postMessage 的第一個引數;origin 屬性表示呼叫window.postMessage() 方法時呼叫頁面的當前狀態; source
屬性記錄呼叫 window.postMessage() 方法的視窗資訊。
語法
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
- 其他視窗的一個引用,比如iframe的contentWindow屬性、執行
message
- 將要傳送到其他 window的資料。它將會被結構化克隆演算法序列化。這意味著你可以不受什麼限制的將資料物件安全的傳送給目標視窗而無需自己序列化。[1]
targetOrigin
通過視窗的origin屬性來指定哪些視窗能接收到訊息事件,其值可以是字串"*"(表示無限制)或者一個URI。在傳送訊息的時候,如果目標視窗的協議、主機地址或埠這三者的任意一項不匹配targetOrigin提供的值,那麼訊息就不會被髮送;只有三者完全匹配,訊息才會被髮送。這個機制用來控制訊息可以傳送到哪些視窗;例如,當用
postMessage傳送密碼時,這個引數就顯得尤為重要,必須保證它的值與這條包含密碼的資訊的預期接受者的orign屬性完全一致,來防止密碼被惡意的第三方截獲。如果你明確的知道訊息應該傳送到哪個視窗,那麼請始終提供一個有確切值的targetOrigin,而不是*。不提供確切的目標將導致資料洩露到任何對資料感興趣的惡意站點。transfer
可選- 是一串和message 同時傳遞的
Transferable
物件. 這些物件的所有權將被轉移給訊息的接收方,而傳送一方將不再保有所有權。
message
必須是一個字串。
從 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)開始,引數 message被
使用結構化克隆演算法進行序列化。這意味著您可以將各種各樣的資料物件安全地傳遞到目標視窗,而不必自己序列化它們。
Gecko 8.0 note
(Firefox 8.0 / Thunderbird 8.0 / SeaMonkey 2.5)
The dispatched event
執行如下程式碼, 其他window可以監聽派遣的message:
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event)
{
// For Chrome, the origin property is in the event.originalEvent
// object.
var origin = event.origin || event.originalEvent.origin;
if (origin !== "http://example.org:8080")
return;
// ...
}
message 的屬性有:
data
- 從其他 window 中傳遞過來的物件。
origin
- 呼叫
postMessage
時訊息傳送方視窗的 origin . 這個字串由 協議、“://“、域名、“ : 埠號”拼接而成。例如 “https://example.org
(implying port443
)”、“http://example.net
(implying port80
)”、“http://example.com:8080
”。請注意,這個origin不能保證是該視窗的當前或未來origin,因為postMessage被呼叫後可能被導航到不同的位置。 source
- 對傳送訊息的視窗物件的引用; 您可以使用此來在具有不同origin的兩個視窗之間建立雙向通訊。
message
必須是一個字串。
從 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)開始,引數 message被
使用結構化克隆演算法進行序列化。這意味著您可以將各種各樣的資料物件安全地傳遞到目標視窗,而不必自己序列化它們。
安全問題
如果您不希望從其他網站接收message,請不要為message事件新增任何事件偵聽器。 這是一個完全萬無一失的方式來避免安全問題。
如果您確實希望從其他網站接收message,請始終使用origin和source屬性驗證發件人的身份。 任何視窗(包括例如http://evil.example.com)都可以向任何其他視窗傳送訊息,並且您不能保證未知發件人不會發送惡意訊息。 但是,驗證身份後,您仍然應該始終驗證接收到的訊息的語法。 否則,您信任只發送受信任郵件的網站中的安全漏洞可能會在您的網站中開啟跨網站指令碼漏洞。
當您使用postMessage將資料傳送到其他視窗時,始終指定精確的目標origin,而不是*。 惡意網站可以在您不知情的情況下更改視窗的位置,因此它可以攔截使用postMessage傳送的資料。
示例
/*
* In window A's scripts, with A being on <http://example.com:8080>:
*/
var popup = window.open(...popup details...);
// When the popup has fully loaded, if not blocked by a popup blocker:
// This does nothing, assuming the window hasn't changed its location.
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");
// This will successfully queue a message to be sent to the popup, assuming
// the window hasn't changed its location.
popup.postMessage("hello there!", "http://example.org");
function receiveMessage(event)
{
// Do we trust the sender of this message? (might be
// different from what we originally opened, for example).
if (event.origin !== "http://example.org")
return;
// event.source is popup
// event.data is "hi there yourself! the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
* In the popup's scripts, running on <http://example.org>:
*/
// Called sometime after postMessage is called
function receiveMessage(event)
{
// Do we trust the sender of this message?
if (event.origin !== "http://example.com:8080")
return;
// event.source is window.opener
// event.data is "hello there!"
// Assuming you've verified the origin of the received message (which
// you must do in any case), a convenient idiom for replying to a
// message is to call postMessage on event.source and provide
// event.origin as the targetOrigin.
event.source.postMessage("hi there yourself! the secret response " +
"is: rheeeeet!",
event.origin);
}
window.addEventListener("message", receiveMessage, false);
注意
任何視窗可以在任何其他視窗訪問此方法,在任何時間,無論文件在視窗中的位置,向其傳送訊息。 因此,用於接收訊息的任何事件監聽器必須首先使用origin和source屬性來檢查訊息的傳送者的身份。 這不能低估:無法檢查origin和source屬性會導致跨站點指令碼攻擊。
與任何非同步排程的指令碼(超時,使用者生成的事件)一樣,postMessage的呼叫者不可能檢測到偵聽由postMessage傳送的事件的事件處理程式何時丟擲異常。
分派事件的origin屬性的值不受呼叫視窗中document.domain的當前值的影響。
僅對於IDN主機名,origin屬性的值不是始終為Unicode或punycode; 在使用此屬性時,如果您期望來自IDN網站的訊息,則最大程度地相容性檢查IDN和punycode值。 這個值最終將始終是IDN,但現在你應該同時處理IDN和punycode表單。
當傳送視窗包含javascript:或data:URL時,origin屬性的值是載入URL的指令碼的
在擴充套件中使用window.postMessage
window.postMessage可用於以chrome程式碼執行的JavaScript(例如,在擴充套件和特權程式碼中),但是分派事件的source屬性總是為空作為安全限制。
(其他屬性具有其期望值。)傳送到位於chrome:URL的視窗的訊息的targetOrigin引數當前被錯誤解釋,使得將導致傳送訊息的唯一值為“*”。 由於此值是不安全的,當目標視窗可以導航到其他地方的惡意網站,建議postMessage不用於與chrome:頁面的溝通; 使用不同的方法(如開啟視窗時的查詢字串)與chrome視窗進行通訊。 最後,在檔案中向頁面釋出訊息:URL當前要求targetOrigin引數為“*”。 file://不能用作安全限制; 這個限制可能會在將來被修改。
規範
瀏覽器相容性
Feature | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari (WebKit) |
---|---|---|---|---|---|
Basic support | 1.0 |
6.0 (6.0)[1] 8.0 (8.0)[2] |
8.0[3] 10.0[4] |
9.5 | 4.0 |
transfer argument |
? | 20.0 (20.0) | 未實現 | ? | ? |
[1] 在 Gecko 6.0 之前 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3), message
引數必須是一個字串。從 Gecko
6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3) 開始,資訊引數使用 結構化克隆演算法 進行序列化。這意味著您可以將各種各樣的資料物件安全地傳遞到目標視窗而無須自行序列化。
[2] Gecko 8.0 引入了對 File
和 FileList
物件進行傳送的支援。
出於安全考慮,這個特性僅在訊息接收者被髮送者包含時生效。
[4] IE10 有重要的限制:詳見本文.