# 同源策略、跨域

# 一、同源策略的介绍

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互,所谓"同源"指的是"三个相同":协议相同,域名相同,端口相同

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。如果非同源,共有三种行为受到限制。

  • Cookie、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

# 二、跨子域

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

document.domain = 'example.com';
1

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法规避同源政策,而要使用下文介绍的PostMessage API。

# 三、跨域窗口

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

对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。

  • 片段识别符(hash)
  • window.name
  • 跨文档通信API(Cross-document messaging)

# 1、片段识别符(hash)

片段标识符指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符。

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
1
2

子窗口通过监听hashchange事件得到通知。

window.onhashchange = checkMessage;
function checkMessage() {
  var message = window.location.hash;
  // ...
}
1
2
3
4
5

同样的,子窗口也可以改变父窗口的片段标识符。

parent.location.href= target + "#" + hash;
1

# 实例

第一个页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>第一个页面</title>
</head>
<body>
 这里是第一个页面
 <iframe src="http://127.0.0.1:8001/index.html"></iframe>
 <button>点击传送数据給页面2</button>
 <script type="text/javascript">
 	 var $btn=document.querySelector('button');
 	 var $iframe=document.querySelector('iframe');
 	 $btn.addEventListener('click',()=>{
 	 	var data={
 	 		name:"zls",
 	 		age:12
 	 	}
 	 	var queryStr='#'
 	 	for(var i in data){
 	 		queryStr+=i+'='+data[i]+'&'
 	 	};
 	 	$iframe.src='http://127.0.0.1:8001/index.html'+queryStr;
 	 },false)
 </script>
</body>
</html>
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

第二个页面(开启本地8001端口服务)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>第二个页面</title>
</head>
<body>
 这里是第二个页面
 <p>从第一个页面来的数据:<span></span></p>
 <script type="text/javascript">
 	window.onhashchange=function(argument) {
 		var data=location.hash.slice(1)
 		document.querySelector('span').innerHTML=data;
 	}
 </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2、window.name

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name属性。

window.name = data;
1

接着,子窗口跳回一个与主窗口同域的网址。

location = 'http://parent.url.com/xxx.html';
1

然后,主窗口就可以读取子窗口的window.name了。

var data = document.getElementById('myFrame').contentWindow.name;
1

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

# 3、window.postMessage

上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。 这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

语法:otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherWindow

其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。

  • message

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

  • targetOrigin

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

举例来说,父窗口http://aaa.com 向子窗口http://bbb.com发消息,调用postMessage方法就可以了。

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
1
2

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。

window.opener.postMessage('Nice to see you', 'http://aaa.com');
1

父窗口和子窗口都可以通过message事件,监听对方的消息。

window.addEventListener('message', function(e) {
  console.log(e.data);
},false);
1
2
3

message事件的事件对象event,提供以下三个属性。

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

下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}
1
2
3
4

event.origin属性可以过滤不是发给本窗口的消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}
1
2
3
4
5
6
7
8
9

# 四、跨域主流方案

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。

  • JSONP
  • WebSocket
  • CORS

# 1、JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。 它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

首先,网页动态插入<script>元素,由它向跨源网址发出请求。

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码通过动态添加<script>元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。

服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

foo({
  "ip": "8.8.8.8"
});
1
2
3

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。

# 手写一个jsonp:

function jsonp(url, jsonpCallbackName,callback) {
  let script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallbackName] = function(data) {
    callback && callback(data)
  }
  document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
  console.log(value)
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2、WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

下面是一个例子,浏览器发出的WebSocket请求的头信息。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
1
2
3
4
5
6
7
8

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通。 如果该域名在白名单内,服务器就会做出如下回应。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
1
2
3
4
5

# 案例

服务端:

// 导入WebSocket模块:
const WebSocket = require('ws');
// 引用Server类:
const WebSocketServer = WebSocket.Server;
// 实例化:
const wss = new WebSocketServer({
    port: 3000
});
// 监听连接
wss.on('connection', function (ws) {
    console.log(`[SERVER] connection()`);
    ws.on('message', function (message) {
        console.log(`[SERVER] Received: ${message}`);
        ws.send(`ECHO: ${message}`, (err) => {
            if (err) {
                console.log(`[SERVER] error: ${err}`);
            }
        });
    })
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

客户端:

// 打开一个WebSocket:
var ws = new WebSocket('ws://localhost:3000/test');
// 响应onmessage事件:
ws.onmessage = function(msg) { console.log(msg); };
// 给服务器发送一个字符串:
ws.send('Hello!');
1
2
3
4
5
6

# 3、CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。cors参考

本文参考

Last Updated: 5/2/2020, 2:51:33 PM