浏览器同源政策及其规避方法(转)

原文地址

最初,同源策略的含义是指,网页A设置的cookie,网页B不能打开,除非这两个网页“同源”,如下:

  • 协议相同
  • 域名相同
  • 端口相同

举例来说,http://www.example.com/dir/page.html的同源情况如下:

  • http://www.example.com/dir2/other.html:同源
  • http://example.com/dir/other.html:不同源(域名不同)
  • http://v2.www.example.com/dir/other.html:不同源(域名不同)
  • http://www.example.com:81/dir/other.html:不同源(端口不同)

同源策略的目的是为了保证用户信息的安全,防止恶意的网站窃取数据。

随着互联网的发展,同源策略越来越严格,目前共有三种行为收到限制:

  1. Cookie、LocalStorage和IndexedDB无法读取;
  2. DOM无法获得;
  3. AJAX请求不能发送。

如何规避

Cookie是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享cookie

举例来说,网页A是http://w1.example.com/a.html,网页B是http://w2.example.com/b.html。那么只要设置相同的document.domain,两个网页就可以共享cookie

注意,这种方法只适用于cookieiframe窗口,LocalStorageIndexedDB是不行的,而要使用PostMessageAPI。

此外,服务器可以在设置cookie的时候,指定cookie所属域名为一级域名,比如.example.com,这样,二级域名和三级域名不用做任何设置,都可以读取这个cookie

iframe

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframewindow.open()打开的窗口与父窗口无法通信。

如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain属性就可以。

对于完全不同源的网站,目前有三种方法:

  • 片段识别符(fragment identifier)
  • window.name
  • 跨文档通信API(cross-document messageing)

片段识别符

片段识别符(fragment identifier)是指URL的#后面的部分,如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息写入子窗口的片段标识符,子窗口监听hashchange事件得到通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父
const src = `${originURL}#${data}`;
document.getElementById('myIframe').src = src;

// 子
window.onhashchange = checkMessage;
function checkMessage() {
const message = window.location.hash;
// ...
}

// 子窗口也可以改变父窗口的片段标识符
parent.location.href = `${target}#${hash}`;

window.name

无论是否同源,只要在一个窗口里,前一个网页设置的window.name,后一个网页就可以读取它。

优点是容量大,可以放置很长的字符串;缺点是必须监听属性变化的事件,影响性能。

postMessage

HTML5提供了跨文档通信API(Cross-document messaging)postMessage,允许跨窗口通信,不论这两个窗口是否同源。

1
2
3
4
5
6
7
8
9
// 父窗口向子窗口
const popup = window.open('http://b.com', 'title');
popup.postMessage('hello', 'http://b.com');

// 子窗口向父窗口
window.opener.postMessage('hello', 'http://a.com');

// 接收数据
window.addEventListener('message', (e) => console.log(e.data), false);

event对象的三个属性:

  • event.source:发送消息的窗口
  • event.origin:消息发向的网址
  • event.data:消息内容

LocalStorage

通过window.postMessage可以读写其他窗口的LocalStorage

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
// 子窗口接收父窗口消息
window.onmessage = (e) => {
if (e.origin !== 'http://bbb.com') return;
const payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
const parent = window.parent;
const data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};

// 父窗口发消息
const win = document.getElementsByTagName('iframe')[0].contentWindow;
const obj = { name: 'Jack' };
// 存入对象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 读取对象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = (e) => {
if (e.origin != 'http://aaa.com') return;
// "Jack"
console.log(JSON.parse(e.data).name);
};

AJAX

AJAX请求只能发给同源网址,有三种方法规避这个限制:

  • JSONP
  • WebSocket
  • CORS