Window: postMessage() 方法

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

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

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

总体而言,一个窗口可以获取另一个窗口的引用(例如,通过 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,而不是 *。未能提供特定目标可能会将数据泄露给恶意站点。

transfer 可选

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

options 可选

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

transfer 可选

transfer 参数具有相同的含义。

targetOrigin 可选

targetOrigin 参数具有相同的含义。

返回值

无(undefined)。

分派的事件

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

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

    // …
  },
  false,
);

分派消息的属性为

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 发送的数据。

安全的共享内存消息传递

如果在与 SharedArrayBuffer 对象一起使用时,postMessage() 抛出异常,您可能需要确保已正确隔离了您的站点。共享内存受两个 HTTP 标头的限制

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

要检查跨源隔离是否成功,您可以针对可用于窗口和工作程序上下文的 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!"
  },
  false,
);
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。(其他属性具有预期值。)

内容或网页上下文脚本无法指定targetOrigin以直接与扩展程序(后台脚本或内容脚本)通信。网页或内容脚本可以使用window.postMessagetargetOrigin"*"来广播到每个监听器,但此方法不建议使用,因为扩展程序无法确定此类消息的来源,并且其他监听器(包括您无法控制的监听器)可以监听。

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

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

规范

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

浏览器兼容性

BCD 表格仅在启用 JavaScript 的浏览器中加载。

另请参阅