postMessage

window.postMessage 跨域通信实战(父容器 ↔ 子页面交互)

window.postMessage() 是浏览器提供的安全跨源通信机制,突破了同源策略(协议、端口、主机需一致)的限制,通过受控的消息传递实现不同页面/窗口间的通信。本文以「父容器 ↔ 子页面」交互为例,实现 Token、用户信息的跨域获取与响应。

一、核心原理

postMessage 实现跨域通信的核心逻辑:

  1. 发送消息targetWindow.postMessage(message, targetOrigin),指定接收窗口和可信域名;
  2. 接收消息:监听 window.addEventListener('message', 回调函数),处理接收到的消息;
  3. 安全校验:必须校验消息来源(event.origin)和消息格式,防止恶意注入。

二、父容器代码(消息接收与响应)

父容器作为消息接收方,监听子页面的请求,根据消息类型返回 Token/用户信息,内置消息格式校验(仅处理 JSON 格式消息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 监听子页面的 postMessage 消息
* @param {MessageEvent} event - 消息事件对象
*/
function receiveMessage(event) {
// 【安全必做】生产环境务必校验消息来源,仅允许可信域名
// if (event.origin.indexOf('yitong.com') === -1 && event.origin.indexOf('allkids.com.cn') === -1) {
// return;
// }

let messageData = { type: '' };
// 校验消息格式:仅处理包含 "dataType":"json" 的字符串消息,避免解析报错
if (event.data && typeof event.data === 'string' && event.data.indexOf('"dataType":"json"') > -1) {
messageData = JSON.parse(event.data);
}

// 根据消息类型响应不同数据
switch (messageData.type) {
case 'getToken':
// 回传 Token 给子页面
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'token',
data: 'this is token' // 替换为实际 Token 字符串
}),
event.origin // 仅向原域名回传,保证安全
);
break;
case 'getUserInfo':
// 回传用户信息给子页面
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'userInfo',
data: { userId: '1234', name: '测试用户' } // 替换为实际用户信息
}),
event.origin
);
break;
default:
break;
}
console.log('父容器接收消息:', event.data);
}

// 注册消息监听
window.addEventListener('message', receiveMessage, false);

三、子页面代码(CMSBridge 通信类)

子页面封装 CMSBridge 类,统一处理消息收发,提供 getTokengetUserInfo 异步方法,支持本地调试和生产环境适配(TypeScript 版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// cms-bridge.ts
interface IMessageData {
type: string;
data?: string | Record<string, any>;
}

class CMSBridge {
token = ''; // 存储获取到的 Token
userInfo!: Record<string, any>; // 存储获取到的用户信息

constructor() {
// 初始化时监听父容器的回传消息
window.addEventListener('message', this.receiveMessage.bind(this), false);
}

/**
* 处理父容器回传的消息
* @param {MessageEvent} event - 消息事件对象
*/
receiveMessage(event: MessageEvent) {
// 【安全必做】生产环境校验消息来源
// if (event.origin.indexOf('yitong.com') === -1 && event.origin.indexOf('allkids.com.cn') === -1) {
// return;
// }

let messageData: IMessageData = { type: '' };
// 校验消息格式,仅处理 JSON 类型消息
if (event.data && typeof event.data === 'string' && event.data.indexOf('"dataType":"json"') > -1) {
messageData = JSON.parse(event.data);
}

// 本地调试标识(localhost 环境)
const isLocalhost = location.href.indexOf('//localhost') > -1;

switch (messageData.type) {
// 接收父容器回传的 Token
case 'token':
this.token = messageData.data as string;
// 向父容器回复确认消息(生产环境替换 * 为实际域名)
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'no reply',
data: `已接收 Token:${messageData.data}`
}),
'*'
);
break;

// 接收父容器回传的用户信息
case 'userInfo':
this.userInfo = messageData.data as Record<string, any>;
// 向父容器回复确认消息
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'no reply',
data: `已接收用户信息:${JSON.stringify(messageData.data)}`
}),
'*'
);
break;

// 本地调试专用:模拟父容器响应(生产环境删除)
case 'getToken':
if (isLocalhost) {
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'token',
data: 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.xxx' // 本地调试 Token
}),
event.origin
);
}
break;
case 'getUserInfo':
if (isLocalhost) {
event.source.postMessage(
JSON.stringify({
dataType: 'json',
type: 'userInfo',
data: { userId: '1234' } // 本地调试用户信息
}),
event.origin
);
}
break;

default:
break;
}
console.log('子页面接收消息:', event.data);
}

/**
* 异步获取 Token(带超时处理)
* @returns {Promise<string>} Token 字符串
*/
getToken(): Promise<string> {
return new Promise((resolve, reject) => {
// 向父容器发送获取 Token 的请求(生产环境替换 * 为实际域名)
window.parent?.postMessage(
JSON.stringify({
dataType: 'json',
type: 'getToken',
data: ''
}),
'*'
);

// 超时处理(3秒超时)
let count = 0;
const timer = setInterval(() => {
count += 10;
// 获取到 Token 则resolve
if (this.token) {
clearInterval(timer);
resolve(this.token);
}
// 超时则reject
if (count > 3000) {
clearInterval(timer);
reject(new Error('获取 Token 超时失败!'));
}
}, 10);
});
}

/**
* 异步获取用户信息(带超时处理)
* @returns {Promise<Record<string, any>>} 用户信息对象
*/
getUserInfo(): Promise<Record<string, any>> {
return new Promise((resolve, reject) => {
// 向父容器发送获取用户信息的请求
window.parent?.postMessage(
JSON.stringify({
dataType: 'json',
type: 'getUserInfo',
data: ''
}),
'*'
);

// 超时处理(3秒超时)
let count = 0;
const timer = setInterval(() => {
count += 10;
if (this.userInfo) {
clearInterval(timer);
resolve(this.userInfo);
}
if (count > 3000) {
clearInterval(timer);
reject(new Error('获取用户信息超时失败!'));
}
}, 10);
});
}
}

export default CMSBridge;

四、子页面使用示例

导入并实例化 CMSBridge,异步获取 Token 和用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 导入 CMSBridge 类
import CMSBridge from '@/api/cms-bridge';

// 实例化通信类
const cms = new CMSBridge();

// 异步获取 Token 和用户信息
async function fetchData() {
try {
const token = await cms.getToken();
const userInfo = await cms.getUserInfo();
console.log('获取到 Token:', token);
console.log('获取到用户信息:', userInfo);
} catch (error) {
console.error('数据获取失败:', error);
}
}

// 执行获取
fetchData();

五、安全注意事项(重中之重)

  1. 严格校验消息来源:生产环境必须放开 event.origin 校验代码,仅允许可信域名(如 yitong.com)的消息,防止恶意网站发送伪造消息;
  2. 限定 targetOriginpostMessage 第二个参数避免使用 *(通配符),生产环境替换为具体的父容器域名(如 https://yitong.com);
  3. 校验消息格式:仅解析包含预期标识(如 "dataType":"json")的消息,避免非法数据导致 JSON.parse 报错;
  4. 超时处理:异步获取数据时添加超时逻辑,避免无限等待。

六、参考资料

总结

  1. postMessage 跨域通信核心是「发送消息 + 监听消息 + 安全校验」,三者缺一不可;
  2. 父容器负责响应子页面请求,子页面封装通信类统一处理消息收发,提升代码复用性;
  3. 生产环境必须强化安全校验(origin + 消息格式),禁止使用 * 作为 targetOrigin,避免安全风险。

postMessage
https://cszy.top/20200728-postMessage/
作者
csorz
发布于
2020年7月28日
许可协议