CORS简介
CORS的全称是Cross-Origin Resource Sharing(跨域资源共享),是基于http1.1的一种跨域解决方案。值得注意的是,CORS对不同请求模式有不同的处理规范,我们将之分为简单请求、非简单请求(不携带cookie和携带cookie又是不一样的情况)。
简单请求
问题:如何定义一个简单请求?
- 请求方法仅限于get、post、head;
- 请求头仅包含安全的字段,常见的安全字段大概是Accept、Accept-Language、Content-Language、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width等。(划重点!请求头如果包含Content-Type,仅限如下的值:text/plain、multipart/form-data、application/x-www-form-urlencoded)
举个例子:
// 简单请求
fetch("https://www.baidu.com");
// 简单请求
fetch("https://www.baidu.com?a=1&b=2");
//不是简单请求,因为请求方法不是三个要求之一
fetch("https://www.baidu.com", {
method:"Delete"
})
//不是简单请求,因为请求头里加入了额外的字段
fetch("http://crossdomain.com/api/news", {
headers:{
a: 1
}
})
//不是简单请求,content-type不满足要求
fetch("http://crossdomain.com/api/news", {
method: "post",
headers: {
"content-type": "application/json"
}
})
问题:发送请求的时候,浏览器做了什么?
- 首先,浏览器会判断他是哪一种请求模式;
- 当判断其为简单请求并且遇到跨域时,会在其请求头里加入Origin字段,Origin字段会告诉服务器,是哪个源地址在发起跨域请求。
所以对于简单请求的跨域问题,需要后端在响应头里加入该字段Access-Control-Allow-Origin:*|具体的源(*代表任何origin的网站都允许跨域)
以express服务器为例,代码实现大致如下:
function(req,res,next)
{
//处理简单请求的跨域
if('origin' in req.headers){
res.header('Access-control-allow-origin',req.header.origin)
}
next()
}
非简单请求
只要请求方法不是get、post、head之一,或者加了自定义的请求头,或者Content-Type不是text/plain、multipart-form-data、application/x-www-form-urlencoded之一就是非简单请求!!!
啊这,对于我们这种经常用Content-Type:application/json的,那就是非简单请求了
举个栗子,以下都是非简单请求:
// 加个自定义header
fetch("https://www.baidu.com",{
method:"post"
header:{
a:1,
b:2,
"content-type":"application/json"
}
});
// 发个delete请求
fetch("https://www.baidu.com",{
method:"delete"
});
问:浏览器发现这不是简单请求后会做何种处理?
- 浏览器会发送一个预检请求,询问服务器是否允许,该请求没有请求体,也不包含我们发起的请求里的请求头。预检请求有几个特征:1.请求方法是options;2.没有请求体;3.请求头中包含了Origin(表示请求的源),Access-Control-Request-Method(表示后续的真实请求会用该方法),Access-Control-Request-Headers(表示后续的真实请求会改动的请求头)
- 服务器在得到该预检请求后,如果没做任何响应,哦吼,不好意思,浏览器的真实请求并不会发送出去,如果服务器允许,那需要服务响应如下的消息格式
...
Access-Control-Allow-Origin:(可以是*号或者具体的源)
Access-Control-Allow-Methods:(预检请求里的方法)
Access-Control-Allow-Headers:a,b,content-type(表示允许改动的请求头)
------------------非必要----------------
Access-Control-Max-Age:毫秒数(该响应头目的在于告诉浏览器允许在某个时效内在该服务器下的跨域请求都不用发起预检,不过在于优化性能上是个不错的选择!)
...
- 浏览器发送真实请求,浏览器带上在真实请求的请求头里加入origin字段
- 服务器响应真实请求,仍然需要Access-Control-Allow-Origin:具体的源或者*号
后端代码实现实例:
function(req,res,next)
{
//处理简单请求的跨域
if('origin' in req.headers){
res.header('Access-Control-Allow-Origin',req.header.origin)
}
//处理预检请求
if(req,method === "OPTIONS"){
req.header("Access-Control-Allow-Methods",req.headers["access-control-request-method"])
req.header("Access-Control-Allow-Headers",req.headers["access-control-request-headers"])
//ps:这里req.headers读取的请求头是小写的,因为去读取该对象会发现对象的key就是小写的,如果大写会读取不到值
}
next()
}
解析:上述代码针对两次请求,都会带上Access-Control-Allow-Origin的字段,因为不管是真实请求或者预检请求,请求头都会携带Origin字段,都会进入后端的第一个判断(是否携带origin)。
问题:为什么需要预检?
- 相容性。比如在那些古老的网站,他们设计之初并不会预想到后来浏览器会发送Delete、Patch等请求,有了这个预检请求后,后端不对这个预检进行处理,浏览器真实请求就不会发送出去,对于后端的处理压力就小了很多。
- 安全性。如果不小心发送了Delete的请求,如果不做预检的话,后端没做任何处理直接接受这个请求并作api处理,那有可能对后端造成不可预期的伤害。(ps:后端一般对于删除、修改等“危险”操作应该会放进一个清单中做权限认证的)
默认情况下,ajax跨域请求并不会携带cookie!
so?我遇到跨域了,我还要携带cookie,怎么配置?

//xhr
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
//fetch api
fetch(url,{
credentials:"include" //默认值为same-origin(表示同域才会带cookie)、omit(表示永远不带)、include(表示都带)
})
你以为这样配置就得了?后端答应了吗?所以后端允许的话需要在响应头里加入"Access-Control-Allow-Credentials":true
function(req,res,next)
{
//处理简单请求的跨域
if('origin' in req.headers){
res.header('Access-Control-Allow-Origin',req.header.origin)
}
//处理预检请求
if(req,method === "OPTIONS"){
req.header("Access-Control-Allow-Methods",req.headers["access-control-request-method"])
req.header("Access-Control-Allow-Headers",req.headers["access-control-request-headers"])
//ps:这里req.headers读取的请求头是小写的,因为去读取该对象会发现对象的key就是小写的,如果大写会读取不到值
}
req.header("Access-Control-Allow-Credentials",true) //允许携带cookie需要加入这个
next()
}
!!!并且后端允许的origin不能再是*号了,而应该是具体的源。如果不作此限制的话,代表任何网站都可以向该api发送请求并携带cookie(啊这?不就是拿你身份证被人拿去借钱,对方不用看借钱的人是不是身份证里的人的一个意思吗)
关于存取自定义的响应头
在跨域访问时,js只能拿到一些最基本的响应头,例如Cache-Control、Content-Type、Content-Language等,加入后端返回的响应头里有个a:1,前端在浏览器network是可以看到的,但是用js去读取的时候输出会是null,这就需要后端响应如下字段(Access-Control-Expose-Headers:a),代表允许读取的响应头。这样js就可以读取响应头里a的值了。