什么是同源策略
浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
“同源”指的是”三个相同“。
- 协议相同
- 域名相同
- 端口相同
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
随着互联网的发展,“同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。
- Cookie、LocalStorage 和 IndexedDB 无法读取。
- DOM 无法获得。
- AJAX 请求无效(可以发送,但浏览器会拒绝接受响应)。
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,将详细介绍,如何规避上面三种限制。
什么是跨域?跨域有几种实现形式
跨域:允许不同域的接口进行交互
实现方式:
- JSONP
- CORS
- 降域
- postMessage
JSONP
JSONP是一种数据调用方式,是解决JSON跨域获取的解决方案。因此JSONP其实是一个非官方的协议。
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
JSONP 的主要缺点有两个,
是只能 GET 不能 POST,因为是通过<script>引用的资源,参数全都显式的放在URL里,和 AJAX 没有关系。
是存在安全隐患,动态插入<script>标签其实就是一种脚本注入,可能遭遇XSS
JSONP原理
网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
JSONP实现
定义数据处理函数_fun
创建script标签,src的地址执行后端接口,最后加个参数callback=_fun
服务端在收到请求后,解析参数,计算返还数据,输出 fun(data) 字符串。
fun(data)会放到script标签做为js执行。此时会调用fun函数,将data做为参数。
<body>
<div class="container">
<ul class="news">
<li>第11日前瞻:中国冲击4金 博尔特再战</li>
<li>男双力争会师决赛 </li>
<li>女排将死磕巴西!</li>
</ul>
<button class="change">换一组</button>
</div>
<script>
$('.change').addEventListener('click', function(){
var script = document.createElement('script'); // 创建元素节点
script.src = 'http://localhost:8080/getNews?callback=appendHtml';
// 引入后端接口 后面加上 callback=fun 确定函数名
document.head.appendChild(script);
// 从后端返回的数据作为js代码执行 调用定义好的函数
document.head.removeChild(script);
})
function appendHtml(news){
var html = '';
for( var i=0; i<news.length; i++){
html += '<li>' + news[i] + '</li>';
}
$('.news').innerHTML = html;
}
function $(id){
return document.querySelector(id);
}
</script>
</body>
app.get('/getNews', function(req, res){
var news = [
"第11日前瞻:中国冲击4金 博尔特再战200米羽球",
"正直播柴飚/洪炜出战 男双力争会师决赛",
"女排将死磕巴西!郎平安排男陪练模仿对方核心",
"没有中国选手和巨星的110米栏 我们还看吗?",
"中英上演奥运金牌大战",
]
var data = [];
for(var i=0; i<3; i++){
var index = parseInt(Math.random()*news.length);
data.push(news[index]);
news.splice(index, 1);
}
var cb = req.query.callback;
if(cb){
res.send(cb + '('+ JSON.stringify(data) + ')');
// JSON.stringify(arr) = "["aa","bb","cc"]"
// '('+ JSON.stringify(data) + ')' = '([data1,data2,data3....])'
}else{ res.send(data); }
})
CORS
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。
相比JSONP只能发GET请求,CORS允许任何类型的请求。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
CORS实现
当你使用 XMLHttpRequest发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理
如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin;
浏览器判断该相应头中是否包含 Origin 的值
如果有则浏览器会处理响应,我们就可以拿到响应数据,
如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
所以 CORS 的表象是让你觉得它与同源的 ajax 请求没啥区别,代码完全一样。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
请求方法是以下三种方法之一。
HEAD
GET
POST
HTTP的头信息不超出以下几种字段。
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
基本流程
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
withCredentials 属性
上面说到,CORS请求默认不包含Cookie信息(以及HTTP认证信息等)。如果需要包含Cookie信息,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
简单例子:
浏览器判断该相应头中是否包含 Origin 的值
如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
//前端
$('.change').addEventListener('click', function () {
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://b.jrg.com:8080/getNews', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
appendHtml(JSON.parse(xhr.responseText))
}
};
window.xhr = xhr
});
结果会发生报错:没有响应头被允许
如果有则浏览器会处理响应,我们就可以拿到响应数据,
//后端
res.header("Access-Control-Allow-Origin", "http://a.jrg.com:8080");
//res.header("Access-Control-Allow-Origin", "*");
可实现CORS跨域:响应头被允许
降域
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。(这种方法又被称为 降域)
举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的DOM。
比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父窗口想获取子窗口的DOM,因为跨域导致报错。
反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body
// 报错
这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。
如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到DOM。
父页面 子页面的script代码里都加上
doucument.domain = 'example.com'
对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。
片段识别符(fragment identifier)
跨文档通信API(Cross-document messaging)
postmessage
HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件对象event,提供以下三个属性。
event.source:发送消息的窗口
event.origin: 消息发向的网址
event.data: 消息内容
下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代码有几个地方需要注意。首先,receiveMessage函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,postMessage方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。
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);
}
}
LocalStorage
通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。
<div class="ct">
<h1>使用postMessage实现跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.jrg.com:8080/a.html">
</div>
<iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe>
</div>
<script>
//URL: http://a.jrg.com:8080/a.html
$('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].postMessage(this.value,'*'); // postMessage
})
window.addEventListener('message',function(e) {
$('.main input').value = e.data
console.log(e.data);
});
function $(id){ return document.querySelector(id); }
</script>
<input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html">
<script>
// URL: http://b.jrg.com:8080/b.html
$('#input').addEventListener('input', function(){
window.parent.postMessage(this.value, '*'); // postMessage
})
window.addEventListener('message',function(e) {
$('#input').value = e.data
console.log(e.data);
});
function $(id){ return document.querySelector(id); }
</script>