Window: postMessage() 方法

Baseline 已广泛支持

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

window.postMessage() 方法可以在 Window 对象之间安全地实现跨源通信;例如,在页面与其生成的弹出窗口之间,或者在页面与其嵌入的 iframe 之间。

通常,不同页面上的脚本只有在它们来自的页面共享相同(也称为“同源策略”)时才能相互访问。window.postMessage() 提供了一种受控机制,可以安全地规避此限制(如果使用得当)。

此外,访问脚本必须事先已获得被访问文档的窗口对象。这可以通过诸如弹出窗口的 window.open() 或 iframe 的 iframe.contentWindow 等方法来实现。

概括地说,一个窗口可以获取对另一个窗口的引用(例如,通过 targetWindow = window.opener),然后使用 targetWindow.postMessage() 在其上分派一个 MessageEvent。接收窗口随后可以根据需要处理此事件。传递给 window.postMessage() 的参数(即“消息”)通过事件对象暴露给接收窗口

语法

js
postMessage(message)
postMessage(message, targetOrigin)
postMessage(message, targetOrigin, transfer)

postMessage(message, options)

参数

message

要分派给另一个窗口的数据。数据使用结构化克隆算法进行序列化。这意味着你可以安全地将各种数据对象传递到目标窗口,而无需自行序列化它们。

targetOrigin 可选

指定接收窗口必须具有的才能接收事件。为了分派事件,源必须完全匹配(包括方案、主机名和端口)。如果省略,则默认为 "/",即调用该方法的源。此机制提供了对消息发送位置的控制;例如,如果使用 postMessage() 传输密码,则此参数必须是一个 URI,其源与包含密码的消息的预期接收者相同,以防止恶意第三方截获密码,这一点绝对至关重要。也可以提供 *,这意味着消息可以分派给具有任何源的侦听器。

注意:如果你知道另一个窗口的文档应该位于何处,请始终提供特定的 targetOrigin,而不是 *。未能提供特定目标可能会将数据泄露给恶意站点。

由于 data: URL 具有不透明源,因此要向具有 data: URL 的上下文发送消息,你必须指定 "*"

transfer 可选

一个可选的可传输对象数组,用于转移所有权。这些对象的所有权将转移到目标方,并且它们在发送方不再可用。这些可传输对象应该附加到消息中;否则它们将被移动,但在接收端实际上无法访问。

options 可选

一个包含以下属性的可选对象

transfer 可选

transfer 参数具有相同的含义。

targetOrigin 可选

targetOrigin 参数具有相同的含义。

返回值

无(undefined)。

分派的事件

一个 window 可以通过执行以下 JavaScript 来侦听分派的消息

js
window.addEventListener("message", (event) => {
  if (event.origin !== "http://example.org:8080") return;

  // …
});

分派消息的属性为

data

从另一个窗口传递的对象。

origin

在调用 postMessage 时发送消息的窗口的。此字符串是协议和“://”、如果存在的主机名以及“:”后跟端口号(如果存在端口并且与给定协议的默认端口不同)的串联。典型源的示例包括 https://example.org(表示端口 443)、http://example.net(表示端口 80)和 http://example.com:8080。请注意,此源保证是该窗口的当前或未来源,该窗口可能自调用 postMessage 后已导航到不同的位置。

source

对发送消息的 window 对象的引用;你可以使用它在两个不同源的窗口之间建立双向通信。

安全问题

如果你不期望收到来自其他站点的消息,请勿message 事件添加任何事件侦听器。这是一种完全避免安全问题的万无一失的方法。

如果你确实期望收到来自其他站点的消息,请始终使用 origin 和可能的 source 属性验证发送者的身份。任何窗口(例如 http://evil.example.com)都可以向当前文档的 iframe 层次结构中从顶部到每个 iframe 下方的任何其他窗口发送消息。但是,在验证身份后,你仍然应该始终验证收到的消息的语法。否则,你信任的发送可信消息的站点中的安全漏洞可能会在你的站点中打开跨站脚本漏洞。

当你使用 postMessage 将数据分派到其他窗口时,请始终指定精确的目标源,而不是 *。恶意站点可以在你不知情的情况下更改窗口的位置,因此它可以截获使用 postMessage 发送的数据。

安全共享内存消息传递

如果将 postMessage()SharedArrayBuffer 对象一起使用时抛出异常,你可能需要确保你的站点已正确进行跨站点隔离。共享内存受两个 HTTP 标头保护

http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

要检查跨源隔离是否成功,你可以针对窗口和 worker 上下文中可用的 Window.crossOriginIsolated 属性进行测试

js
const myWorker = new Worker("worker.js");

if (crossOriginIsolated) {
  const buffer = new SharedArrayBuffer(16);
  myWorker.postMessage(buffer);
} else {
  const buffer = new ArrayBuffer(16);
  myWorker.postMessage(buffer);
}

示例

js
/*
 * 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 dispatched 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!"
});
js
/*
 * 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,
  );
});

注意

窗口中文档中的任何脚本都可以通过在该窗口对象上调用 .postMessage() 来请求将消息分派到其已获得窗口对象的另一个窗口中的文档。因此,用于接收消息的任何事件侦听器必须首先使用 origin 和可能的 source 属性检查消息发送者的身份。这怎么强调都不为过:未能检查 origin 和可能的 source 属性会导致跨站脚本攻击。

与任何异步分派脚本(超时、用户生成的事件)一样,postMessage 的调用者无法检测侦听 postMessage 发送的事件的事件处理程序何时抛出异常。

调用 postMessage() 后,MessageEvent仅在所有待处理的执行上下文完成后才分派。例如,如果在事件处理程序中调用 postMessage(),则该事件处理程序以及该相同事件的任何剩余处理程序将运行完成,然后才分派 MessageEvent

分派事件的 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 属性始终为 null。(其他属性具有其预期值。)

内容或 Web 上下文脚本无法指定 targetOrigin 以直接与扩展程序(无论是后台脚本还是内容脚本)通信。Web 或内容脚本可以使用 window.postMessagetargetOrigin"*" 进行广播,但这是不鼓励的,因为扩展程序无法确定此类消息的来源,并且其他侦听器(包括你无法控制的侦听器)可以窃听。

内容脚本应使用 runtime.sendMessage 与后台脚本通信。Web 上下文脚本可以使用自定义事件与内容脚本通信(如果需要,使用随机生成的事件名称,以防止来宾页面窃听)。

最后,向 file: URL 处的页面分派消息目前要求 targetOrigin 参数为 "*"file:// 不能用作安全限制;此限制将来可能会修改。

规范

规范
HTML
# dom-window-postmessage-options-dev

浏览器兼容性

另见