什么是跨域?
概念:指从一个域名的网页向另一个网页去请求资源,只要协议、域名、端口有任何一个不同,都被当作是跨域。例如:
url | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js |
相同协议,同一域名 | 允许 |
https://www.a.com/a.js http://www.a.com/a.js |
不同协议,同一域名 | 不允许 |
http://www.a.com/a.js http://www.b.com/a.js |
相同协议,不同域名 | 不允许 |
http://js.a.com/a.js http://www.b.com/a.js |
相同协议,不同域名 | 不允许 |
http://www.a.com:8080/a.js http://www.a.com/b.js |
主域相同,子域不同 | 不允许 |
http://www.a.com/a.js http://70.23.92.75/a.js |
域名和域名对应ip | 不允许 |
为什么浏览器要限制跨域访问呢?
本质上来说是浏览器的同源策略所限制的,主要是防止恶意网页访问私人信息或损害用户的数据。
为什么要跨域?
既然跨域访问存在安全问题,那为什么又要跨域呢?因为有时候公司内部会有多个不同子域,比如 http://www.a.com 和 http://www.b.com, 从 http://www.a.com 访问 http://www.b.com 的资源就会跨域。还有一种情况就是调用一些外部的API,也需要跨域。
如何跨域?
首先纠正一个误区,跨域并非浏览器限制了发起跨站请求的这种能力,恰恰相反,我们可以发出请求,服务端也可以接收到请求并正常返回数据,只不过在返回之后浏览器会阻止非同源数据(response),从而在控制台打出一系列报错信息。
主要有5种跨域方法,我们会分别介绍其工作原理并举例说明,最后对比各个方法的优缺点。
1 JSON-P
JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据,它的工作原理在于script
标签不受同源策略限制,并且请求得到script资源后会立即执行。
-
浏览器端
首先在浏览器端注册一个回调函数,它的参数是期望服务器端返回的数据,这个回调函数具体内容就是处理这些数据。
function show(callback) {
//处理数据
}
然后动态地添加script
标签,src地址为:请求资源的地址+回调函数名称,这里的回调函数名称是与服务器端约定好的。
$('#change').addEventListener('click',function(){
var script=document.createElement('script')
script.src='http://localhost:8080/getMusic?callback=show'
document.head.appendChild(script)
document.head.removeChild(script)
})
-
服务器端
首先从url中获取回调函数名称,看是否存在回调函数,如果存在的话,会动态的生成JavaScript代码片段(例如show([data1,data2……])
),然后发送数据。
var cb=req.query.callback
if (cb) {
res.send(cb+'('+JSON.stringify(data)+')')
} else {
res.send(data)
}
-
执行
浏览器端收到返回的数据,作为参数传入回调函数show(),然后立即执行这个JavaScript,这样就能根据之前写好的回调函数处理这些数据。
完整代码戳这里 ==> JSON-P
2 CORS
CORS全称是跨域资源共享(Cross-origin resource sharing),是一种ajax跨域请求资源的方式,支持现代浏览器,IE支持10以上。
CROS实现方式很简单,当你使用XMLHttpRequest发送请求时,浏览器发现该请求 不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理,如果确定接受请求则在返回结果中加一个响应头:Access-Control-Allow-Origin;浏览器判断该相应头中是否包含Origin的值,如果有,浏览器就会处理响应,我们就可以拿到响应数据,如果不包含浏览器直接驳回,这是我们无法拿到响应数据,所以使用 CORS 跨域的时候其实和普通的 ajax 过程是一样的,只是浏览器在发现这是一个跨域请求的时候会自动帮我们处理一些事,比如验证等等,所以说只要服务端提供支持,前端是不需要做额外的事情的。
var data =[];
for (var i = 0; i < 5; i++) {
var index = parseInt(Math.random()*musicList.length)
data.push(musicList[index])
musicList.splice(index,1)
}
res.header("Access-Control-Allow-Origin", "*")
res.send(data);
只需要在服务器端加上Access-Control-Allow-Origin
,它的值是请求时Origin字段的值或者 *
,*
表示接受任意域名的请求。
完整代码戳这里 ==> CROS
2种请求(转载自https://zhuanlan.zhihu.com/p/24198444)
-
简单请求
若请求满足所有下述条件,则该请求可视为“简单请求”:
使用下列方法之一:
GET
HEAD
POSTHTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Content-Type
DRP
Downlink
Save-Data
Viewport-Width
WidthContent-Type的值属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
简单请求不会触发 CROS预检请求
过程:
对于简单的跨域请求,浏览器会自动在请求的头信息加上 Origin
字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端会获取到这个值,然后判断是否同意这次请求并返回。
// 请求
GET /cors HTTP/1.1
Origin: https://api.qiutc.me
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
情况一:服务端允许
如果服务端许可本次请求,就会在返回的头信息多出几个字段:
// 返回
Access-Control-Allow-Origin: https://api.qiutc.me
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Info
Content-Type: text/html; charset=utf-8
这三个带有 Access-Control
开头的字段分别表示:
- Access-Control-Allow-Origin:必须,它的值是请求时Origin字段的值或者
*
,*
表示接受任意域名的请求。 - Access-Control-Allow-Credentials:可选,它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。
再需要发送cookie的时候还需要注意要在AJAX请求中打开 withCredentials 属性:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为*
,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
- Access-Control-Expose-Headers: 可选。CORS请求时,XMLHttpRequest对象的
getResponseHeader()
方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('Info')
可以返回Info字段的值。
情况二:服务端拒绝
当然我们为了防止接口被乱调用,需要限制源,对于不允许的源,服务端还是会返回一个正常的HTTP回应,但是不会带上 Access-Control-Allow-Origin
字段,浏览器发现这个跨域请求的返回头信息没有该字段,就会抛出一个错误,会被 XMLHttpRequest
的 onerror
回调捕获到。
这种错误无法通过 HTTP 状态码判断,因为回应的状态码有可能是200。
- 非简单请求
条件:除了简单请求以外的CORS请求。
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是 application/json。
过程:
1)预检请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
预检请求的发送请求:
OPTIONS /cors HTTP/1.1
Origin: https://api.qiutc.me
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.qiutc.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
预检请求的返回:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: https://api.qiutc.me
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
-
Access-Control-Allow-Methods
必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
2)浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
3 document.domain
浏览器的同源策略,其限制之一就是我们说的不能通过ajax的方法去请求不同源中的资源。 它的第二个限制是浏览器中不同域的页面框架(frame)之间是不能进行js的交互操作的。
// 当前页面域名:http://a.mac.com:8080/a.html
<div class="ct">
<h1>使用降域实现跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.mac.com/a.html">
</div>
<iframe src="http://b.mac.com/b.html" frameborder="0" ></iframe>
</div>
不同域的页面框架可以获取彼此的window对象,但是无法获取属性和值。这个时候document.domian就有用了,只需要将a.mac.com 和b.mac.com 的document.domain都设置为相同的mac.com就可以了。
需要注意的是:
document.domain
的设置是有限制的,我们只能把document.domian
设置为自身或者更高一级的父域,并且主域必须相同。
4 POSTmessage
window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源。兼容性:
使用方法:
//URL: http://a.jrg.com:8080/a.html
$('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].postMessage(this.value,'*');
})
window.addEventListener('message',function(e) {
$('.main input').value = e.data
console.log(e.data);
});
该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。
各种方法优缺点比较
方法 | 优点 | 缺点 |
---|---|---|
JSONP | 兼容性更好,不需要XMLHttpRequest或ActiveX的支持 | 只能通过GET方式请求,一方面是参数长度有限制,二是安全性比较差;后端需要知道前端的cb是什么样的结构,主要在参数和回调名;后端需要进行参数和cb的拼接然后才能执行; |
cros | 前端比较方便,只需要发送请求即可;安全性能够得以控制和保障 | 兼容性不全面,需要做降级处理 |
document.domain | 可以实现不同window之间的相互访问和操作 | 只适用于父子window之间的通信,不能用于xhr;只能在主域相同且子域不同的情况下使用 |
postMessage | 不需要后端介入,简单快捷,一个函数外加两个参数(请求url,发送数据)就可以搞定 | 无法做到一对一的传递方式:监听中需要做很多消息的识别,由于postMessage发出的消息对于同一个页面的不同功能相当于一个广播的过程,该页面的所有onmessage都会收到,所以需要做消息的判断; 安全性问题:三方可以通过截获,注入html或者脚本的形式监听到消息,从而能够做到篡改的效果,所以在postMessage和onmessage中一定要做好这方面的限制; 发送的数据会通过结构化克隆算法进行序列化,所以只有满足该算法要求的参数才能够被解析,否则会报错,如function就不能当作参数进行传递; |