今天小编介绍下安全地实现对象之间的跨域通信方法:Window.postMessage()。
window.postMessage()
方法可以安全地实现对象之间的跨域通信Window
;例如,在页面和它生成的弹出窗口之间,或者在页面和嵌入其中的 iframe 之间。
通常,当且仅当它们源自的页面共享相同的协议、端口号和主机(也称为"同源策略")时,不同页面上的脚本才被允许相互访问。 window.postMessage()
提供了一种受控机制来安全地规避此限制(如果使用得当)。
广义上讲,一个窗口可以获取对另一个窗口的引用(例如,通过 targetWindow = window.opener
),然后 MessageEvent
在其上使用调度一个targetWindow.postMessage()
。然后接收窗口可以根据需要自由处理此事件。传递给window.postMessage()
(即"消息")的参数通过事件对象暴露给接收窗口。
语法 {#syntax}
postMessage(message, targetOrigin)
postMessage(message, targetOrigin, transfer)
参数 {#parameters}
-
message
{#message} {#message}
-
要发送到另一个窗口的数据。使用 结构化克隆算法对数据进行序列化。这意味着您可以将各种数据对象安全地传递到目标窗口,而无需自己序列化它们。
-
targetOrigin
{#targetorigin} {#targetorigin}
-
指定要调度的事件必须是此窗口的来源,或者作为文字字符串
"*"
(表示无偏好)或作为 URI。如果在调度事件时,此窗口文档的方案、主机名或端口与 中提供的不匹配,targetOrigin
则不会调度该事件;只有当所有三个都匹配时,才会调度事件。这种机制提供了对消息发送位置的控制;例如,如果postMessage()
用于传输密码,则该参数必须是一个 URI,其来源与包含密码的消息的预期接收者相同,以防止恶意第三方拦截密码。如果您知道其他窗口的文档应位于何处,请始终提供特定的targetOrigin
,而不是。*
未能提供特定目标会泄露您发送到任何感兴趣的恶意网站的数据。 -
transfer
可选的{#transfer} {#transfer}
-
与消息一起传输 的一系列可传输对象。这些对象的所有权被授予目标端,它们在发送端不再可用。
返回值 {#return_value}
无 ( undefined
)。
发送的事件 {#the_dispatched_event}
Awindow
可以通过执行以下 JavaScript 来监听分派的消息:
window.addEventListener("message", (event) => {
if (event.origin !== "http://example.org:8080")
return;
// …
}, false);
分派消息的属性是:
-
data
{#data} {#data}
-
从另一个窗口传递的对象。
-
origin
{#origin} {#origin}
-
调用了当时发送消息的窗口 的来源。
postMessage
该字符串是协议和"://"的串联,如果存在主机名,则":"后跟端口号,如果端口存在并且与给定协议的默认端口不同。典型起源的例子是https://example.org
(暗示端口443
)、http://example.net
(暗示端口80
)和http://example.com:8080
。请注意,不能postMessage
保证此原点是该窗口的当前或未来原点,它可能在调用后 已导航到不同的位置 。 -
source
{#source} {#source}
-
window
对发送消息的对象 的引用;您可以使用它在具有不同来源的两个窗口之间建立双向通信。
安全问题 {#security_concerns}
如果您不希望收到来自其他站点的消息,请不要为事件添加任何事件侦听器message
。这是避免安全问题的完全万无一失的方法。
如果您确实希望接收来自其他站点的消息,请始终使用和可能 的属性验证发件人的身份。任何窗口(例如,包括 )都可以向任何其他窗口发送消息,并且您无法保证未知发件人不会发送恶意消息。然而,在验证了身份之后,您仍然应该始终验证接收到的消息的语法。否则,您信任的仅发送受信任消息的站点中的安全漏洞可能会在您的站点中打开跨站点脚本漏洞。 origin``source``http://evil.example.com
*
当您用于 postMessage
将数据发送到其他窗口时,请始终指定确切的目标原点,而不是。恶意站点可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用postMessage
.
安全共享内存消息传递 {#secure_shared_memory_messaging}
如果postMessage()
在与对象一起使用时抛出异常SharedArrayBuffer
,您可能需要确保跨站点正确隔离您的站点。共享内存位于两个 HTTP 标头后面:
-
Cross-Origin-Opener-Policy
具有same-origin
价值(保护您的来源免受攻击者的侵害) -
Cross-Origin-Embedder-Policy
具有require-corp
价值(保护受害者免受您的起源)
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
要检查跨源隔离是否成功,您可以针对 crossOriginIsolated
window 和 worker 上下文可用的属性进行测试:
if (crossOriginIsolated) {
// Post SharedArrayBuffer
} else {
// Do something else
}
举个例子
/*
* In window A's scripts, with A being on http://example.com:8080:
*/
const 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.com");
window.addEventListener("message", (event) => {
// Do we trust the sender of this message? (might be
// different from what we originally opened, for example).
if (event.origin !== "http://example.com")
return;
// event.source is popup
// event.data is "hi there yourself! the secret response is: rheeeeet!"
}, false);
/*
* In the popup's scripts, running on http://example.com:
*/
// Called sometime after postMessage is called
window.addEventListener("message", (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);
});
笔记
任何窗口都可以在任何其他窗口上访问此方法,无论文档在窗口中的位置如何,都可以随时向其发送消息。因此,任何用于接收消息的事件侦听器都必须origin
首先使用和可能的source
属性检查消息发送者的身份。这一点怎么强调都不为过:未能检查origin
和可能的source
属性会导致跨站点脚本攻击。
与任何异步分派的脚本(超时、用户生成的事件)一样,调用者postMessage
无法检测到监听由 发送的事件的事件处理程序何时postMessage
抛出异常。
AfterpostMessage()
被调用,只有在所有挂起的执行上下文完成后MessageEvent
才会被调度。例如,如果在事件处理程序中调用,则该事件处理程序将运行完成,同一事件的任何剩余处理程序将在调度之前运行。
调度事件的origin
属性值不受document.domain
调用窗口中当前值的影响。
仅对于 IDN 主机名,该origin
属性的值与 Unicode 或 punycode 不一致;如果您期望来自 IDN 站点的消息,则在使用此属性时对 IDN 和 punycode 值进行最大的兼容性检查。该值最终将始终是 IDN,但现在您应该同时处理 IDN 和 punycode 形式。
origin
当发送窗口包含一个 javascript:
或URL 时 ,该属性的值data:
是加载该 URL 的脚本的来源。
在扩展中使用 window.postMessage
window.postMessage
可用于在 chrome 代码中运行的 JavaScript(例如,在扩展和特权代码中),但source
调度事件的属性始终null
作为安全限制。(其他属性有其预期值。)
内容或 Web 上下文脚本无法指定 a targetOrigin
直接与扩展程序(后台脚本或内容脚本)通信。Web 或内容脚本可以使用 window.postMessage
a targetOrigin
of"*"
向每个听众广播,但不鼓励这样做,因为扩展无法确定此类消息的来源,而其他听众(包括您无法控制的听众)可以收听。
内容脚本应该用于runtime.sendMessage
与后台脚本进行通信。Web 上下文脚本可以使用自定义事件与内容脚本进行通信(如果需要,使用随机生成的事件名称,以防止从访客页面窥探)。
最后,在file:
URL 的页面上发布消息当前需要targetOrigin
参数为"*"
. file://
不能用作安全限制;将来可能会修改此限制。