CORS 跨域数据交互

跨域数据交互

在实际项目中,ajax 经常应用在跨域通信的场景下,如果采用 jsonp 来实现跨域,需要对前后端代码有很大的改动。而且, jsonp 仅支持 get 请求
跨域。为此我们必须寻找一个简单的跨域通信方式 - CORS。

什么是 CORS

CORS是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制。使用CORS,可以通过普通的XMLHttpRequest发起请求和获得数据,并且支持各种类型请求。说白了就是利用XMLHttpRequest来实现跨站通信,而不是仅仅遵循同源策略,再也不用为了ajax能跨域写苦逼的jsonp了。 CORS技术现在已经被广泛的支持了。 兼容性如下:
js12-1.png
CORS 兼容大部分浏览器, IE8 及其以下就自求多福吧。

CORS 使用方法及场景举例

所谓的跨域请求,从本质上来说并不是浏览器对其他域名的请求不能发送,而是请求可以正常发起,
但是浏览器在收到服务器信息后就屏蔽掉了,并向前端报错(跨域)。如图所示,跨域请求结果被屏蔽。
js12-2.png
解决方案,服务器在响应HTTP头部加入 Access-Control-Allow-Origin:*即可,这样就表示服务端同意任意域名的请求。
一般我们会指定可响应的域名如:Access-Control-Allow-Origin: http://b.com, http://c.com等。这样浏览器在检测到服务端HTTP头部的时候就可以不再拦截响应。
请求头部:
js12-3.png
响应头部:
js12-4.png
就在我们以为轻松搞定的时候,我们又遇到了问题。前端要给后端传json格式的数据于是在http头部加入了
content-type: application/json, access_token等信息。请求再次失败,通过观察控制台信息我们发现,多了一个请求options:
js12-5.png
原来在CORS中的请求分为两种:简单请求和复杂请求

简单请求

  1. 只使用GET,HEAD或者POST。如果使用POST来发送数据到服务器,那么使用HTTP POST请求发送到服务器的数据的Content-Type为以下几种之一:application/x-www-form-urlencoded,multipart/form-data以及text/plain。
  2. 不使用HTTP请求发送定制请求头(例如X-Modified等)

在简单请求下,我们只需要像上面所说设置Access-Control-Allow-Origin头部即可。

复杂请求

  1. 使用了除GET,HEAD和POST以外的方法。如果使用POST方法发送请求数据时的Content-Type不是application/x-www-form-urlencoded,multipart/form-data或者text/plaint。例如,如果POST请求向服务器使用application/xml或者text/xml向服务器发送请求,那么这个请求就是preflighted的。
  2. 设置了定制请求头的请求(例如,请求使用了例如X-PINGOTHER这样的请求头)。这类请求在发送正式请求之前会发送一个Preflighted(预请求)Preflighted请求首先通过HTTP OPTIONS方法请求其他域上的资源,以确定发送实际的请求是否安全。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。我们第二次发的请求是一个复杂请求,服务端没有响应options的方法,导致预请求失败,之后的请求也就终止了。我们看一下复杂请求下的HTTP报文:

Prelignted请求头部:
js12-6.png
可以看到报文头部信息为OPTIONS请求
预请求服务端响应报文
js12-7.png
预请求响应只要返回一个2xx表示成功的响应信息即可说明服务端同意了跨域请求,
这里我们选择204的状态码作为响应状态。因为204表示响应成功,并且没有结果返回的状态。这样节省了传输信息的事件,加快了预请求的处理。
预处理结束后,事情并没有就此解决。错误信息再次传来。

原来我们自定义了头部信息,还要在响应报文中加入Access-Control-Allow-Headers来指出服务端允许的复杂请求需要的自定义头部信息。注意这里不能用*来表示所有,
只能一个个添加。例如:"Access-Control-Allow-Methods" : "PUT,POST,GET,DELETE,OPTIONS"
这样就齐活了,总算是数据可以流通了。这里我建议在服务端响应部分也加入 "Access-Control-Allow-Methods" : "PUT,POST,GET,DELETE,OPTIONS"来指出可接受的请求类型。
最后我们的服务端响应头部信息:

附带凭证信息的请求

我手贱的在xhr中加入了 xhr.withCredentials = true 因为跨源请求默认请求头部中不提供凭据(cookie、HTTP认证及客户端SSL证明等)。
为了能把这些信息带上,我们设置了withCredentials为true。浏览器再次报错,通过检查我们发现,
如果你设置了withCredentials为true那么 Access-Control-Allow-Origin就不能用 * ,必须使用明确的域名。
而且还要为你的响应头加上"Access-Control-Allow-Credentials":"true"信息。
最后我列出CORS中可能使用的头部信息供大家参考:

  • HTTP响应头:Access-Control-Allow-Origin,Access-Control-Expose-Headers,Access-Control-Max-Age,
    Access-Control-Allow-Credentials,Access-Control-Allow-Methods,Access-Control-Allow-Headers

  • HTTP请求头:Origin,Access-Control-Request-Method,Access-Control-Request-Headers

附上用Node.js跨域请求服务端的简单配置:

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
const express = require("express"),
app = express();
//统一拦截请求设置头部信息,响应预请求
app.all('*', function(req, res, next) {
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type,access_token,user_id",
"Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS"
});
if(req.method=="OPTIONS") {
res.send(204);
}else {
next();
}
});
app.post("/data", function(req, res) {
res.json({
status: "success",
text: "返回数据"
})
});
app.listen(3000, "127.0.0.1", function() {
console.log("服务器启动了");
});

跨域请求的其他方案

这里列举一下其他常用的跨域方案,仅供参考:

  1. jsonp:利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。
  2. postMessage:postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
  3. websocket:WebSocket实现了全双工通信,使WEB上的真正的实时通信成为可能。浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
  4. SSE:Server-Sent Events(SSE)功能,允许服务端推送数据到客户端(通常叫数据推送)。已经在浏览器上普遍支持,然而IE和Edge全系列不支持。
  5. ServiceWorker:一个 service worker 是一段运行在浏览器后台进程里的脚本,它独立于当前页面,提供了那些不需要与web页面交互的功能在网页背后悄悄执行的能力。在将来,基于它可以实现消息推送,静默更新以及地理围栏等服务,但是目前它首先要具备的功能是拦截和处理网络请求,包括可编程的响应缓存管理。简而言之,这是让浏览器具有服务器功能的API,有了他还跨域个球球。不过这只是实验中API目前只有chrome和firfox高版本浏览器支持。