这一次,彻底搞懂跨域之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. 包含了自定义的请求头,比如Authorization, X-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文档应该就可以很快的理解这个中间件的用法。

总结

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

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

相关推荐
卸任7 小时前
Vibe coding工具使用心得
前端·ai编程
brien98 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance8 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M8 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写9 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李9 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易9 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马9 小时前
前沿视界
前端
kyriewen9 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易9 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员