这一次,彻底搞懂跨域之CORS解决方案

在大概很多年以前,那时的我还是个初出茅庐的大学生,两大框架,前端基础了解的都还可以。平时前端方面的问题也可以解决个七七八八。但是在前后端请求通信这一块,只有一些粗浅的八股文知识。前后两三次遇到跨域问题,问了当时的领导,都是告诉我交给后端解决。于是当时就给我一种固有观念,跨域嘛,后端问题。

直到有一次,遇到一个跨域请求(没错,其他请求都没跨域,只有这一个请求跨域了)。按照惯性思维,交给后端解决。快下班了,后端告诉我,你这个请求为啥是OPTIONS请求?

看着这个也是刚入职不久的年轻人,我缓缓的问出了一个问题:你毕业多久了。

他说:快一年了,怎么了?

丸辣!!!

于是在两个人查了许多资料没解决问题之后决定,要不上线试试?由于当时项目部署是前后端不分离的,所以上线之后竟然意外的没有问题。两个人内心只有一个想法:下班,反正代码还能跑。

几年后,我的同事又重新遇到这个问题,刚好最近在研究nodejs,看到了express这部分。于是我的内心,就有一种冲动:

这一次,彻底搞懂如何使用CORS解决这个问题

1. 为什么会跨域?

既然要解决跨域,我们要知道为什么会跨域:

浏览器出于安全策略,限制了一个源的文档或者脚本与另一个源的交互行为,只有同源的交互才是不被限制的。

同源的判定: 如果两个 URL 的协议端口(如果有指定的话)和主机都相同的话,则这两个 URL 是同源

有了这个概念,我们就知道,我们就知道因为我们浏览器访问的页面在一个域名下,而要访问的接口在另一个域名下资源(ajax请求),所以浏览器觉得你们可能不认识,所以禁止了这个行为。

2. 跨域问题的解决

这个时候,如果有人能出示一下凭证,证明一下关系可以信任,浏览器还是没有那么死板的。而这个时候,最让浏览器可信的,当然是服务端的态度:就像你要去别人家拜访,请求方再怎么花言巧语,开不开门还是主人家说的算。

这个交互就叫做CORS,全称是Cross-Origin Resource Sharing,中文翻译为跨域资源共享。即当出现跨域问题的时候,只要服务端允许(服务端通过一定的方式通知客户端表明当前请求可以获取资源),那么浏览器就可以访问跨域资源。

在发送请求的时候,一个请求可能会携带很多信息,所以对服务端的影响也不一样。

针对不同的请求,CORS制定了三种不同的交互模式:

  1. 简单请求
  2. 需要预检请求
  3. 需要附带身份凭证的请求

简单请求

简单请求的判定:

  1. 请求方法必须是:GET、POST、HEAD。
  2. 请求头中仅包含安全字段。安全字段包括:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  1. 如果请求头包含了Content-Type,那么他的值只能是application/x-www-form-urlencoded,multipart/form-data,text/plain

同时满足了上述条件的请求,就会被判定为简单请求。

当产生一个简单请求的时候,浏览器发送请求时,请求标头会自动带上Origin字段,该字段向服务端表明,是哪个源域的请求。

服务端接收到该请求后,会在响应头标明一个access-control-allow-origin字段,该字段标明哪些域是允许跨域访问的。

  1. 如果access-control-allow-origin的值是*,则表示允许所有域的请求。
  2. 如果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();
})

需要预检请求

如果一个请求超出了简单请求的判定,如:

  1. 请求方法不是GET、POST、HEAD,
  2. 包含了自定义的请求头,比如AuthorizationX-Requested-With等,
  3. 请求头中包含Content-Type,且它的值不是application/x-www-form-urlencoded,multipart/form-data,text/plain

那么这个请求就会被判定成为复杂请求,在发送之前,浏览器会发送一个预检请求,复杂请求交互的流程:

  1. 浏览器首先会发送一个预检请求,询问服务器是否允许: 比如有以下请求:
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字段,该字段的值就是后续请求的请求头中包含的自定义字段
  1. 服务器判断是否允许该请求,如果允许,那么服务端需要对每一个特殊加上的请求头作出回应,任意一个没有作出回应,或者回应对不上,那么就表示不允许,返回的响应头如下:
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秒内,同样的请求(三个消息都一样),都可以不用发送预检请求

预检请求的响应没有消息体,只有一个类似上面的响应头

  1. 浏览器发送后续真实请求,真实请求和简单请求流程一样,只携带一个origin字段

  2. 服务器响应真实的消息体。

服务端处理复杂请求的方式:

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文档应该就可以很快的理解这个中间件的用法。

总结

当年的自己因为一心只求最终的解决方案,所以导致查阅了很多文章都没能搞懂这个问题。

现在的自己再遇到问题,已经慢慢学会去追查其本质,从本质上去解决这个问题。通过这一系列了解,相信下一次遇到类似的问题,不管是自己解决,还是帮同事解决,都可以手到擒来。

相关推荐
八了个戒1 分钟前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
noravinsc30 分钟前
html页面打开后中文乱码
前端·html
小满zs1 小时前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦1 小时前
聊聊双列瀑布流
前端·javascript·面试
键指江湖2 小时前
React 在组件间共享状态
前端·javascript·react.js
诸葛亮的芭蕉扇2 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a2 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序
徐小夕3 小时前
花了2个月时间研究了市面上的4款开源表格组件,崩溃了,决定自己写一款
前端·javascript·react.js
by————组态3 小时前
低代码 Web 组态
前端·人工智能·物联网·低代码·数学建模·组态
拉不动的猪3 小时前
UniApp金融理财产品项目简单介绍
前端·javascript·面试