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屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames。
message
- 將要發送到其他 window的數據。它將會被結構化克隆算法序列化。這意味着你可以不受什麼限制的將數據對象安全的傳送給目標窗口而無需自己序列化。[1]
targetOrigin
通過窗口的origin屬性來指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示無限制)或者一個URI。在發送消息的時候,如果目標窗口的協議、主機地址或端口這三者的任意一項不匹配targetOrigin提供的值,那麼消息就不會被髮送;只有三者完全匹配,消息纔會被髮送。這個機制用來控制消息可以發送到哪些窗口;例如,當用
postMessage傳送密碼時,這個參數就顯得尤爲重要,必須保證它的值與這條包含密碼的信息的預期接受者的orign屬性完全一致,來防止密碼被惡意的第三方截獲。如果你明確的知道消息應該發送到哪個窗口,那麼請始終提供一個有確切值的targetOrigin,而不是*。不提供確切的目標將導致數據泄露到任何對數據感興趣的惡意站點。transfer
可選- 是一串和message 同時傳遞的
Transferable
對象. 這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
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,請不要爲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://不能用作安全限制; 這個限制可能會在將來被修改。
規範
規範 | 狀態 | 註釋 |
---|---|---|
HTML
Living Standard window.postMessage |
Living Standard | No change from HTML5 Web Messaging |
HTML5
Web Messaging window.postMessage |
Recommendation | Initial definition. |
瀏覽器兼容性
[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
對象進行傳送的支持。
出於安全考慮,這個特性僅在消息接收者被髮送者包含時生效。
[3] IE8 和 IE9 僅支持 <frame>
和 <iframe>
之間的通信。
[4] IE10 有重要的限制:詳見本文.