在大概很多年以前,那时的我还是个初出茅庐的大学生,两大框架,前端基础了解的都还可以。平时前端方面的问题也可以解决个七七八八。但是在前后端请求通信这一块,只有一些粗浅的八股文知识。前后两三次遇到跨域问题,问了当时的领导,都是告诉我交给后端解决。于是当时就给我一种固有观念,跨域嘛,后端问题。
直到有一次,遇到一个跨域请求(没错,其他请求都没跨域,只有这一个请求跨域了)。按照惯性思维,交给后端解决。快下班了,后端告诉我,你这个请求为啥是OPTIONS
请求?
看着这个也是刚入职不久的年轻人,我缓缓的问出了一个问题:你毕业多久了。
他说:快一年了,怎么了?
丸辣!!!
于是在两个人查了许多资料没解决问题之后决定,要不上线试试?由于当时项目部署是前后端不分离的,所以上线之后竟然意外的没有问题。两个人内心只有一个想法:下班,反正代码还能跑。
几年后,我的同事又重新遇到这个问题,刚好最近在研究nodejs
,看到了express
这部分。于是我的内心,就有一种冲动:
这一次,彻底搞懂如何使用CORS解决这个问题
1. 为什么会跨域?
既然要解决跨域,我们要知道为什么会跨域:
浏览器出于安全策略,限制了一个源的文档或者脚本与另一个源的交互行为,只有同源的交互才是不被限制的。
有了这个概念,我们就知道,我们就知道因为我们浏览器访问的页面在一个域名下,而要访问的接口在另一个域名下资源(ajax请求),所以浏览器觉得你们可能不认识,所以禁止了这个行为。
2. 跨域问题的解决
这个时候,如果有人能出示一下凭证,证明一下关系可以信任,浏览器还是没有那么死板的。而这个时候,最让浏览器可信的,当然是服务端的态度:就像你要去别人家拜访,请求方再怎么花言巧语,开不开门还是主人家说的算。
这个交互就叫做CORS,全称是Cross-Origin Resource Sharing,中文翻译为跨域资源共享。即当出现跨域问题的时候,只要服务端允许(服务端通过一定的方式通知客户端表明当前请求可以获取资源),那么浏览器就可以访问跨域资源。
在发送请求的时候,一个请求可能会携带很多信息,所以对服务端的影响也不一样。
针对不同的请求,CORS制定了三种不同的交互模式:
- 简单请求
- 需要预检请求
- 需要附带身份凭证的请求
简单请求
简单请求的判定:
- 请求方法必须是:GET、POST、HEAD。
- 请求头中仅包含安全字段。安全字段包括:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- 如果请求头包含了Content-Type,那么他的值只能是
application/x-www-form-urlencoded
,multipart/form-data
,text/plain
同时满足了上述条件的请求,就会被判定为简单请求。
当产生一个简单请求的时候,浏览器发送请求时,请求标头会自动带上Origin
字段,该字段向服务端表明,是哪个源域的请求。
服务端接收到该请求后,会在响应头标明一个access-control-allow-origin
字段,该字段标明哪些域是允许跨域访问的。
- 如果
access-control-allow-origin
的值是*
,则表示允许所有域的请求。 - 如果
access-control-allow-origin
的值与浏览器请求中Origin字段的值一致,表明当前请求也是可访问的。
但是对于浏览器来说,响应头中access-control-allow-origin
的值不论是*
还是与浏览器请求中Origin字段的值一致,没有什么差别,浏览器只关心当前
所以对于简单请求,可以通过express的中间件方式处理,加上对应的响应头即可:
js
const express = require('express');
const app = express();
const allowOriginList = ['http://localhost:8080'];
app.use((req, res, next) => {
if("Origin" in req.headers) {
let origin = req.headers.Origin;
if(allowOriginList.includes(origin)) {
res.header('Access-Control-Allow-Origin', Origin);
}
}
next();
})
需要预检请求
如果一个请求超出了简单请求的判定,如:
- 请求方法不是GET、POST、HEAD,
- 包含了自定义的请求头,比如
Authorization
,X-Requested-With
等, - 请求头中包含
Content-Type
,且它的值不是application/x-www-form-urlencoded
,multipart/form-data
,text/plain
那么这个请求就会被判定成为复杂请求,在发送之前,浏览器会发送一个预检请求,复杂请求交互的流程:
- 浏览器首先会发送一个预检请求,询问服务器是否允许: 比如有以下请求:
js
fetch('http://localhost:3000/api/login', {
method: "POST",
headers: {
a: 1,
b: 2,
Content-Type: 'application/json'
},
body: JSON.stringify({name: 'zhangsan'})
})
那么经过浏览器处理之后,请求报文中会产生如下格式:
http
OPTIONS /api/login HTTP/1.1
Host: localhost:3000
...
Origin: http://localhost:8080 // 请求源域
Access-Control-Request-Method: POST // 请求方法
Access-Control-Request-Headers: a, b, Content-Type
预检请求的目的是询问服务器,是否允许后续的请求,他不包含请求体,只包含了之后请求要做的事。 预检请求的特征:
- 请求方法为OPTIONS
- 没有请求体
- 请求头中包含
Origin
字段,该字段的值就是当前请求的源域 - 请求头中包含
Access-Control-Request-Method
字段,该字段的值就是后续请求的请求方法 - 请求头中包含
Access-Control-Request-Headers
字段,该字段的值就是后续请求的请求头中包含的自定义字段
- 服务器判断是否允许该请求,如果允许,那么服务端需要对每一个特殊加上的请求头作出回应,任意一个没有作出回应,或者回应对不上,那么就表示不允许,返回的响应头如下:
http
HTTP/1.1 200 OK
...
// 每一个都需要对应
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Method: POST
Access-Control-Allow-Headers: a, b, Content-Type
Access-Control-Max-Age: 86400 // 接下来86400秒内,同样的请求(三个消息都一样),都可以不用发送预检请求
预检请求的响应没有消息体,只有一个类似上面的响应头
-
浏览器发送后续真实请求,真实请求和简单请求流程一样,只携带一个origin字段
-
服务器响应真实的消息体。
服务端处理复杂请求的方式:
js
const express = require('express');
const app = express();
app.use((req, res, next) => {
if(req.method === 'OPTIONS') {
// 这是一个预检请求,要检测三个
let methods = req.headers['access-control-request-method'];
let headers = req.headers['access-control-request-headers'];
if(methods && headers) {
// res.header('Access-Control-Allow-Origin', req.headers.Origin); // 允许的源域
res.header('Access-Control-Allow-Method', methods);
res.header('Access-Control-Allow-Headers', headers);
}
}
if("Origin" in req.headers) {
let origin = req.headers.Origin;
if(allowOriginList.includes(origin)) {
res.header('Access-Control-Allow-Origin', Origin);
}
}
next();
})
需要附带身份凭证的请求
默认情况下,跨域请求不会携带Cookie,当一些请求需要鉴权时,必须携带Cookie。但是携带Cookie可能会对服务器造成更大的影响,所以如果请求中需要携带Cookie,需要对请求进行配置:
在请求时:
js
fetch('http://localhost:3000/api/login', {
credentials: 'include', // omit代表不携带Cookie, include代表携带Cookie, same-origin代表同源的请求才携带Cookie
})
Cookie通常是一个用户的身份凭证,所以携带了Cookie的跨域请求,需要更严格的配置,服务端需要明确告诉客户端,允许携带Cookie。
允许的方式就是在相应的时候添加一个响应头:Access-Control-Allow-Credentials: true
。若没有明确告知客户端,则该请求也被视为不被允许的跨域请求。
如果一个跨域请求,规定了需要携带身份凭证,那么这个请求的响应头中,
Access-Control-Allow-Origin
的值不能是*
,必须是当前请求的源域。
跨域的中间件
以上函数可以不用自己实现,实现的目的是了解CORS的原理,express中提供了cors
中间件,可以简化上述的实现:
shell
npm install cors
如果全部允许跨域那么只需要设置:
js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
通过上面的讲解,再查看cors文档应该就可以很快的理解这个中间件的用法。
总结
当年的自己因为一心只求最终的解决方案,所以导致查阅了很多文章都没能搞懂这个问题。
现在的自己再遇到问题,已经慢慢学会去追查其本质,从本质上去解决这个问题。通过这一系列了解,相信下一次遇到类似的问题,不管是自己解决,还是帮同事解决,都可以手到擒来。