CORS 是浏览器跨域限制解决的一种方案。我们来使用 JavaScript 的前端框架 Vue 和服务端框架 Koa2 来进行理解 CORS 是如何解决跨域问题的。
注:我们使用的前后端的域名端口如下,可以看出来是属于跨域的。
前端 http://localhost:3000
后端 http://127.0.0.1:3002
1.origin 跨域
我们首先来使用前端 Vue 的工具库 Axios 向后端服务发起一个 get 请求。
- 前端
Axios.defaults.baseURL = 'http://127.0.0.1:3002'
Axios.get('/user').then((res) => {
console.log(res.data);
})
//请求的后端地址:http://127.0.0.1:3002/user
- 后端
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.body = {name:'leon'}
})
由上面的截图可以看出前端向后端发送请求跨域了,并且提示说被请求的资源没有设置 Access-Control-Allow-Origin 响应头字段。我们看后端代码确实也没有设置这个字段。让我们来修改一下后端代码。
- 后端
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','http://localhost:3000')
ctx.body = {name:'leon'}
})
修改成允许 http://localhost:3000 的客户端跨域获取资源之后,跨域的错误提示消失,并且成功打印返回值。当然 Access-Control-Allow-Origin 可以设置某个域名也可以设置为 * 号,意思是接收任何域名的请求。
总结:上面演示之后我们可以看出,当前端和后端出现跨域时,也就是协议+域名+端口三者存在不一致时,如果后端没有允许 origin 跨域请求,那么虽然请求能正常请求和响应,但是响应回来之后会被浏览器的安全策略拦截。
思考:怎么来验证是被浏览器安全策略拦截了呢?
我们可以使用其他的 http 客户端,比如 postman。去除后端设置 Access-Control-Allow-Origin 的代码之后,我们用 postman 请求,发现是可以正常请求的,由此可以验证。
2.请求数据跨域
预备知识:
在讲解请求数据跨域之前,我们得认识一个概念,叫预检请求(preflight)。预请求指的是我们在发起正式请求之前,如果这个请求属于复杂请求,那么会事先发送一个 options 请求给服务器来进行预先判断服务器支不支持当前请求,如果出现 500 或者 403 等错误就不会发起真正的请求了。
那么哪些情况会发起预请求呢?
- 请求的方法不是 GET/HEAD/POST
- 请求的 Content-Type 不是 application/x-www-form-urlencoded, multipart/form-data, 或 text/plain
- 请求设置了自定义的 header 字段等等。
接下来我们来验证一下。
Axios.get('/user').then((res) => {
console.log(res.data);
})
上面请求为 get 请求,并且没有额外的一些配置,因此不会发送预检请求。
Axios.get('/user',{
headers:{
'custom-header':'hello'
}
}).then((res) => {
console.log(res.data);
})
我们来修改一下代码,给 get 请求加上自定义请求头,那么结果如何呢?从下面的截图中可以看出,此次发送了两个请求,一个为 preflight,也就是预检请求,请求类型为 options,由浏览器自行发起,一个为我们真实的请求(虽然跨域报错了,但是确实发起了)。
接下来我们来看看请求数据跨域的场景。
Axios.post('/user',{
age:18
}).then((res) => {
console.log(res.data);
})
当我们使用 post 请求来传递一些数据给服务端的时候,我们看一下请求头。可以看出 Content-Type 为 application/json,不包含在以上预请求 Content-Type 类型中,于是我们会发现跨域问题又出现了。错误提示为:请求头字段 Content-Type 不被预请求的 Access-Control-Allow-Headers 所允许。
那么我们要怎么解决这个问题呢?
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','http://localhost:3000')
ctx.set('Access-Control-Allow-Headers','Content-Type')
ctx.body = {name:'leon'}
})
答案是设置响应头的 Access-Control-Allow-Headers 字段为 Content-Type,也就是允许该请求头字段跨域,如上面代码所示。
于是数据又可以正常响应了。
总结:当我们前端传递除 application/x-www-form-urlencoded, multipart/form-data, 或 text/plain 之外的数据类型,比如 json 给后端时,会产生跨域问题,我们可以通过后端设置支持 Content-Type 请求头支持跨域解决问题。
3.请求方法跨域
平时除了我们常用的请求方法 get,post 以外,可能还会使用 put,delete 等类型的请求。我们就来使用一下 put 请求。
- 前端
Axios.put('/user',{
age:18
}).then((res) => {
console.log(res.data);
})
- 后端
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','http://localhost:3000')
ctx.set('Access-Control-Allow-Headers','Content-Type')
ctx.body = {name:'leon'}
})
请求之后发现又跨域了...错误提示为:PUT 类型的请求方法不被预请求的 Access-Control-Allow-Methods 支持。
那么这个问题又该怎么解决呢?
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','http://localhost:3000')
ctx.set('Access-Control-Allow-Headers','Content-Type')
ctx.set('Access-Control-Allow-Methods','PUT')
ctx.body = {name:'leon'}
})
答案其实也不难,我们可以设置响应头 Access-Control-Allow-Methods 的值为 PUT,也就是支持 PUT 类型的请求跨域。
于是我们又发现能正常获取到响应值了。
4.自定义请求头跨域
如果标准的请求头不能实现需求时,我们可能会用到自定义请求头。比如我们可以用自定义请求头来标注一下当前请求在什么平台下发出。
- 前端
Axios.put('/user',{
age:18
},{
headers:{
'Platform':'WEB'
}
}).then((res) => {
console.log(res.data);
})
这时候我们惊喜的发现,又跨域了。错误提示为:Platform 请求头字段不被预请求的 Access-Control-Allow-Headers 头字段允许。
这个错误应该比较熟悉,没错,上面解决 Content-Type 跨域时也有类似的错误。于是我们自然能想到解决方案。
- 后端
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','http://localhost:3000')
ctx.set('Access-Control-Allow-Headers','Content-Type,Platform')
ctx.set('Access-Control-Allow-Methods','PUT')
ctx.body = {name:'leon'}
})
我们在响应的 Access-Control-Allow-Headers 中添加 Platform 即可。
棒,我们又可以正常的获取到响应了!
5.cookie 跨域
在验证 cookie 跨域处理之前,我们需要做点准备工作,我们需要使用代理。
https://b.test.com http://localhost:3000/ #前端代理
https://a.test.com http://127.0.0.1:3002/ #后端代理
此时前端访问 b.test.com 相当于访问了 http://localhost:3000/,后端同理。
- 前端
Axios.defaults.baseURL = 'https://a.test.com'// 代理的后端地址,相当于访问http://127.0.0.1:3002/
Axios.put('/user',{
headers:{
'Platform':'WEB',
},
}).then((res) => {
console.log(res.data);
})
- 后端
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','https://b.test.com')//前端代理的地址,相当于http://localhost:3000/
ctx.set('Access-Control-Allow-Headers','Content-Type,Platform')
ctx.set('Access-Control-Allow-Methods','PUT')
ctx.set('Set-Cookie','id=123;path=/;domain=test.com')
ctx.body = {name:'leon'}
})
在上面的代码进行请求之后,我们看看结果。我们发现请求没有任何报错,但是也发现 cookie 并没有写入浏览器。
- 前端
Axios.defaults.baseURL = 'https://a.test.com'// 代理的后端地址,相当于访问http://127.0.0.1:3002/
Axios.put('/user',{
headers:{
'Platform':'WEB',
},
withCredentials:true
}).then((res) => {
console.log(res.data);
})
我们对前端做如上改动,允许 cookie 跨域传输,看一下结果。出现了跨域,错误提示的大意是:预请求的响应中没有设置 Access-Control-Allow-Credentials,导致了 cookie 跨域。
我们对后端也做一定的修改。
- 后端
app.use(ctx => {
ctx.set('Access-Control-Allow-Origin','https://b.test.com')//前端代理的地址,相当于http://localhost:3000/
ctx.set('Access-Control-Allow-Headers','Content-Type,Platform')
ctx.set('Access-Control-Allow-Methods','PUT')
ctx.set('Access-Control-Allow-Credentials',true)
ctx.set('Set-Cookie','id=123;path=/;domain=test.com')
ctx.body = {name:'leon'}
})
设置 Access-Control-Allow-Credentials 的字段为 true,于是又能看见熟悉的响应结果,并且 cookie 也被成功种入浏览器。