什么是CORS
跨域资源共享 是一种利用额外的HTTP头告诉浏览器让运行再一个origin(domain)上的web应用被准许访问来自不用源服务器上的指定资源,当一个资源从与该资源本身所在服务器不同域,资源就会发起一个跨域请求
浏览器限制从脚本内发起的跨域HTTP请求,比如XMLreq,FetchAPI
意味着只能从同一个域请求HTTP资源 除非响应报文包含了正确的?CORS响应头
CORS原理
跨域资源共享标准
新增
了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)
从上面的文字中我们得到如下信息:
1、跨域资源共享标准新增了一组 HTTP 首部字段,服务器通过这些字段来控制浏览器有权访问哪些资源。
2、为了安全起见请求方式分为两类,一类不会预先发送options请求,一些会预先发送options请求。
3、 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求会触发options请求。
4、服务器验证OPTIONS完成后才会允许发送世界的http请求。
不会触发http预检请求的便是简单请求,想法能够触发http预检请求的便是复杂请求。
简单请求
满足:
1 .请求方法是以下三种方法之一:
HEAD
GET
POST
2.HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
3.请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
4、请求中没有使用 ReadableStream 对象
简单请求的部分响应头及解释如下:
Access-Control-Allow-Origin
(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写"*"。
Access-Control-Allow-Credentials
(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。
Access-Control-Expose-Headers
(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得如下的信息:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
当你需要访问额外的信息时,就需要在这一项当中填写并以逗号进行分隔
如果仅仅是简单请求,那么即便不用CORS也没有什么大不了,
那什么是复杂请求呢,除了简单请求都是复杂请求。
非简单请求就是复杂请求。
复杂请求在正式请求前都会有预检请求,在浏览器中都能看到有OPTIONS请求,用于向服务器请求权限信息的。
axios 都是复杂请求,ajax 可以是简单请求
简单请求的发送从代码上来看和普通的XHR没太大区别,但是HTTP头当中要求总是包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,并不是开发者代码可以触及到的。
但CORS的复杂请求就令CORS显得更加有用了。简单来说,任何不满足上述简单请求要求的请求,都属于复杂请求。比如说你需要发送PUT、DELETE等HTTP动作,或者发送Content-Type: application/json的内容。
复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种"预请求",此时作为服务端,也需要返回"预回应"作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。
预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容
- Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUT、DELETE等等。
- Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。
显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。
复杂请求的部分响应头及解释如下:
Access-Control-Allow-Origin
(必含) – 和简单请求一样的,必须包含一个域。
Access-Control-Allow-Methods
(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
Access-Control-Allow-Headers
(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可。
Access-Control-Allow-Credentials
(可选) – 和简单请求当中作用相同
Access-Control-Max-Age
(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。
理论聊完之后,咱们来看一下实践,首先启动两个服务,一个端口为3000,的静态资源服务器,用于请求接口,另一台端口为5000的接口服务器,如图所示:
const express = require("express");
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended:false}));
app.use(bodyParser.json());
// 实现CORS
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,DELETE');
res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization");
res.header("cache-control", "no-cache");
res.header("content-type", "application/json; charset=utf-8");
res.header("ETag", '');
next();
});
app.post("/p",(req,res)=>{
res.send(req.body)
})
app.listen(5000,()=>{
console.log("5000")
})
axios.post("http://localhost:5000/p",{name:"zs",age:"18"}).then((data)=>{
console.log(data.data);
})
我们用axios这个http请求库发送了一个post请求,axios发送post请求默认会把数据转化为json格式,并且会默认设置请求头:Content-Type:application/json
,很显然这是一个复杂请求,这样的话,会触发options请求。
我们分别启动两个服务,并打开浏览器,访问页面,加载请求接口脚本,观察network如图:
我们看到确实发送了两次请求一次为OPTIONS一次为POST,而我们代码中并没有处理对OPTIONS请求的响应处理,所以上面服务端代码是不合理的,综合考虑,OPTIONS请求并会对实际http请求产生影响,所以我们统一的对OPTIONS请求返回204,服务端负责支持CORS的中间件修正代码如下:
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,DELETE');
res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization");
res.header("cache-control", "no-cache");
res.header("content-type", "application/json; charset=utf-8");
res.header("ETag", '');
//header头信息设置结束后,结束程序往下执行,返回
if(req.method.toLocaleLowerCase() === 'options'){
res.status(204);
return res.json({}); //直接返回空数据,结束此次请求
}else{
next();
}
});
我们在中间件中判断请求方式,如果请求方式为OPTIONS返回状态码204,并返回空信息。
如果我们将请求脚本改成如下代码:
axios.post("http://localhost:5000/p","name=zs&age=18",{headers:{
"Content-Type":"application/x-www-form-urlencoded",
}}).then((data)=>{
console.log(data.data);
})
或者又改成如下代码:
axios.post("http://localhost:5000/p","name=zs&age=18",{headers:{
"Content-Type":"application/x-www-form-urlencoded",
"clm":"234"
}}).then((data)=>{
console.log(data.data);
})
为什么跨域的post请求区分为简单请求和非简单请求和content-type相关?
什么content-type为下面三种的却被视为简单请求,工作中碰到的post请求很多都会采用下面的格式,且会修改数据库内容,其不会存在安全风险吗?还是说对于存在安全隐患的接口,应该采用application/json格式。
text/plain
multipart/form-data
application/x-www-form-urlencoded
preflight的引入是为了解决什么问题呢?
简单请求就是普通 HTML Form 也可以发出的请求,比如表单的 method 如果指定为 POST ,可以用 enctype 属性指定用什么方式对表单内容进行编码,合法的值就是你题目里提到的这三种。
非简单请求就是普通 HTML Form 无法实现的请求。比如 PUT 方法、需要其他的内容编码方式、自定义头之类的。
对于服务器来说,第一,许多服务器压根没打算给跨源用。当然你不给 CORS 响应头,浏览器也不会使用响应结果,但是请求本身可能已经造成了后果。所以最好是默认禁止跨源请求。
第二,要回答某个请求是否接受跨源,可能涉及额外的计算逻辑。这个逻辑可能很简单,比如一律say yes。也可能比较复杂,结果可能取决于哪个资源哪种操作来自哪个origin。对浏览器来说,就是某个资源是否允许跨源这么简单;对服务器来说,计算成本却可可大可小。所以我们希望最好不用每次请求都让服务器劳神计算。
CORS-preflight 就是这样一种机制,浏览器先单独请求一次,询问服务器某个资源是否可以跨源,如果不允许的话就不发实际的请求。注意先许可再请求等于默认禁止了跨源请求。如果允许的话,浏览器会记住,然后发实际请求,且之后每次就都直接请求而不用再询问服务器否可以跨源了。于是,服务器想支持跨源,就只要针对 preflight 进行跨源许可计算。本身真正的响应代码则完全不管这个事情。并且因为 preflight 是许可式的,也就是说如果服务器不打算接受跨源,什么事情都不用做。
但是这机制只能限于非简单请求。在处理简单请求的时候,如果服务器不打算接受跨源请求,不能依赖 CORS-preflight 机制。因为不通过 CORS,普通表单也能发起简单请求,所以默认禁止跨源是做不到的。
既然如此,简单请求发 preflight 就没有意义了,就算发了服务器也省不了后续每次的计算,反而在一开始多了一次 preflight。
补充:关于『向下兼容』】
把简单请求不需要preflight理解为『向下兼容』也不能说错。但严格来说,并不是『为了向下兼容』而不能发。理论上浏览器可以区别对待表单请求和非表单请求 —— 对传统的跨源表单提交不发preflight,从而保持兼容,只对非表单跨源请求发preflight。
但这样做并没有什么好处,反而把事情搞复杂了。比如本来你可以直接用脚本发跨源普通请求,尽管(在服务器默认没有跨源处理的情况下)你无法得到响应结果,但是你的需求可能只是发送无需返回,比如打个日志。但现在如果服务器不理解preflight你就干不了这个事情了。
而且如果真的这样做,服务器就变成了默认允许跨源表单,如果想控制跨源,还是得(跟原本一样)直接在响应处理中执行跨源计算逻辑;另一方面服务器又需要增加对preflight请求的响应支持,执行类似的跨源计算逻辑以控制来自非表单的相同跨源请求。服务器通常没有区分表单/非表单差异的需求,这样搞纯粹是折腾服务器端工程师。
所以简单请求不发preflight不是因为不能兼容,而是因为兼容的前提下发preflight对绝大多数服务器应用来说没有意义,反而把问题搞复杂了。