同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

**同源定义: ** 两个页面的Protocol(协议)Port(端口)host(域名)相同

例如:http://www.aaa.com/dir/page.html 协议: ** http://端口: 80(默认)域名:** www.aaa.com

1
2
3
4
5
"http://www.aaa.com/dir/other.html" :同源
"http://aaa.com/dir/other.html": 不同源(域名不同)
"http://v2.www.aaa.com/dir/other.html": 不同源(域名不同)
"http://www.aaa.com:8081/dir/other.html": 不同源(端口不同)
"https://www.aaa.com:8081/dir/other.html": 不同源(协议不同)

同源策略下的跨页面通讯

Broadcast Channel API

Broadcast Channel API 可以实现同 下浏览器不同窗口,Tab页,frame或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面)之间的简单通讯。

Note: 此特性在 Web Worker 中可用。

广播频道会被命名和绑定到指定的源。

通过创建一个监听某个频道下的 BroadcastChannel 对象,你可以接收发送给该频道的所有消息。一个有意思的点是,你不需要再维护需要通信的 iframe 或 worker 的索引。它们可以通过构造 BroadcastChannel 来简单地“订阅”特定频道,并在它们之间进行全双工(双向)通信。

例子🌰

1
2
3
4
5
6
7
8
9
10
11
// 连接到广播频道
var bc = new BroadcastChannel('test_channel');

// 发送简单消息的示例 参数可以是任意对象
bc.postMessage('This is a test message.');

// 简单示例,用于将事件打印到控制台
bc.onmessage = function (ev) { console.log(ev); }

// 断开频道连接
bc.close()
  • BroadcastChannel() - 构建函数用于创建一个 BroadcastChannel 对象,并与对应的频道相关联。
  • BroadcastChannel.onmessage - 当 BroadcastChannel 接收到类型为 MessageEventmessage 事件时,**BroadcastChannel.onmessage** 属性可以指定一个函数,作为该事件对应的事件处理程序来执行。
  • BroadcastChannel.postMessage() - 可以使用 BroadcastChannel.postMessage() 发送一条任意 Object 类型的消息,给所有同下监听了该频道的所有浏览器上下文。消息以 message 事件的形式发送给每一个绑定到该频道的广播频道。
  • BroadcastChannel.close() - 通过调用 BroadcastChannel.close() 方法,可以马上断开其与对应频道的关联,并让其被垃圾回收。这是必要的步骤,因为浏览器没有其它方式知道频道不再被需要。

SharedWorker

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。SharedWorker 被多个页面实例化时, 数据是共享的.

例子🌰

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
// page 1
var myWorker = new SharedWorker("worker.js");

//如果worker.js已经用 addEventListener 监听了 onmessage 事件,则可以使用 start() 方法手动启动端口
// myWorker.port.start();

first.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

second.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

myWorker.port.onmessage = function(e) {
result1.textContent = e.data;
console.log('Message received from worker');
console.log(e.lastEventId);
}

// page 2
var myWorker = new SharedWorker("worker.js");

// 如果worker.js已经用 addEventListener 监听了 onmessage 事件,则可以使用 start() 方法手动启动端口
// myWorker.port.start();
squareNumber.onchange = function() {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log('Message posted to worker');
}

myWorker.port.onmessage = function(e) {
result2.textContent = e.data;
console.log('Message received from worker');
}

// worker.js
onconnect = function(e) {
var port = e.ports[0];

port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
}

// onconnect = function(e) {
// var port = e.ports[0];
// port.addEventListener('message', function(e) {
// var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
// port.postMessage(workerResult);
// });
//
// port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
// }

contentWindow

contentWindow 属性返回当前HTMLIFrameElementWindow对象. 你可以使用这个Window 对象去访问这个iframe的文档和它内部的DOM. 这个是可读属性, 但是它的属性像全局Window 一样是可以操作的.

1
2
3
4
5
var x = document.getElementsByTagName("iframe")[0].contentWindow;
//x = window.frames[0];

x.document.getElementsByTagName("body")[0].style.backgroundColor = "blue";
// this would turn the 1st iframe in document blue.

Web Storage API

Web Storage API 提供了存储机制,通过该机制,浏览器可以安全地存储键值对.

Web Storage 包含如下两种机制:

  • sessionStorage 为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
  • localStorage 同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。

这两种机制是通过 Window.sessionStorageWindow.localStorage 属性使用(更确切的说,在支持的浏览器中 Window 对象实现了 WindowLocalStorageWindowSessionStorage 对象并挂在其 localStoragesessionStorage 属性下)—— 调用其中任一对象会创建 Storage 对象,通过 Storage 对象,可以设置、获取和移除数据项。对于每个源(origin)sessionStoragelocalStorage 使用不同的 Storage 对象——独立运行和控制。

通过 Web Storage中的通过 StorageEvent 响应存储的变化 ,可以做到同源下的跨页面的数据同步。

1
2
3
4
5
6
7
8
9
10
window.addEventListener('storage', function (e) {
if (e.key === 'ctc-msg') {
const data = JSON.parse(e.newValue);
const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
console.log('[Storage I] receive message:', text);
}
});

mydata.st = +(new Date);
window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));

无论何时,Storage 对象发生变化时(即创建/更新/删除数据项时,重复设置相同的键值不会触发该事件Storage.clear() 方法至多触发一次该事件),StorageEvent 事件会触发。在同一个页面内发生的改变不会起作用——在相同域名下的其他页面(如一个新标签或 iframe)发生的改变才会起作用。在其他域名下的页面不能访问相同的 Storage 对象。

跨源通讯

postMessage

window.postMessage() 方法可以安全地实现跨源通信。对于两个不同页面的脚本,一般情况下需要满足同源策略才能进行数据通讯。但window.postMessage()方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口

1
otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow - 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames

  • message - 将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。[1]

  • targetOrigin - 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”“(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。*如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。

例子🌰

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
/*
* A窗口的域名是<http://example.com:8080>,以下是A窗口的script标签下的代码:
*/

var popup = window.open(...popup details...);

// 如果弹出框没有被阻止且加载完成

// 这行语句没有发送信息出去,即使假设当前页面没有改变location(因为targetOrigin设置不对)
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");

// 假设当前页面没有改变location,这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");

function receiveMessage(event)
{
// 我们能相信信息的发送者吗? (也许这个发送者和我们最初打开的不是同一个页面).
if (event.origin !== "http://example.org")
return;

// event.source 是我们通过window.open打开的弹出页面 popup
// event.data 是 popup发送给当前页面的消息 "hi there yourself! the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 弹出页 popup 域名是<http://example.org>,以下是script标签中的代码:
*/

//当A页面postMessage被调用后,这个function被addEventListener调用
function receiveMessage(event)
{
// 我们能信任信息来源吗?
if (event.origin !== "http://example.com:8080")
return;

// event.source 就当前弹出页的来源页面
// event.data 是 "hello there!"

// 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source
// 作为回信的对象,并且把event.origin作为targetOrigin
event.source.postMessage("hi there yourself! the secret response " +
"is: rheeeeet!",
event.origin);
}

window.addEventListener("message", receiveMessage, false);

MessageEvent

1
2
3
4
5
window.addEventListener("message", receiveMessage, false) ;
function receiveMessage(event) {
var origin= event.origin;
console.log(event);
}

event对象的打印结果截图如下:

这里重点介绍event对象的四个属性

  • data : 指的是从其他窗口发送过来的消息对象;
  • type: 指的是发送消息的类型;
  • source: 指的是发送消息的窗口对象;
  • origin: 指的是发送消息的窗口的源

Refer To

https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

https://juejin.cn/post/6844903811232825357

https://juejin.cn/post/6844903665694687240