一文说清CORS

545 阅读9分钟

本文是我结合使用CORS过程的个人理解,以及参考了MDN文档、其他网友的文章写的。如有描述不当之处,敬请各位指出纠正!

一、什么是CORS

1.1、CORS简介

CORS,全称是 “跨域资源共享” (Cross-origin resource sharing)。是一个W3C标准。它新增了一组HTTP标头字段,来允许服务器声明哪些源站通过浏览器有权限访问哪些资源。 (这些HTTP标头是CORS配置的重点!!)

目的是消除各种 API 中的「同源限制」,以便可以在不同源(即服务器)之间共享资源

CORS的产生,源于浏览器的同源策略,限制了网页无法向非同源地址发送AJAX请求。但实际上,如今很多网页应用都需要向非同源的服务器请求数据,于是为了突破这个限制,允许在服务器一侧配置一些CORS规则,来告诉浏览器允许哪些非同源的请求可以通过。

1.2、浏览器是如何限制非同源请求的

首先,浏览器能够自动识别请求是否同源。然后,对于非同源请求,浏览器根据服务端返回的响应头相关字段(Access-Control-Allow-Origin等)决定是否同意该跨域请求正常进行。

所以,CORS的控制核心在于后端的设置。服务端主动制定规则,浏览器被动执行规则。

Host表示请求的服务器地址,Origin表示请求发出的地址。这两个字段对比就可以判断请求是否同源

网页发送跨域请求时,浏览器自动在requeset header上添加了origin字段,其值为网页的域(协议、域名、端口,但不包含URL的路径和锚点)

服务端没有设置CORS相关配置,返回的reponse header中没有 Access-Control-Allow-Origin 等相关字段

服务端配置了CORS,返回的 response header 中包含了这些配置字段

服务端配置了CORS,返回的 response header 中包含了Access-Control-Allow-Origin字段,但是与origin并不匹配



二、CORS工作机制(可先跳过)

在 CORS 通信中,所有请求分为两类:简单请求、非简单请求。(这两个名词已经被现行的CORS规范废弃了,但是这里为了区分两者,我们仍然使用这个名字)。

如果觉得这两者难以区分,没有关系,因为这对我们使用CORS并没有实际的影响。这里区分两者只是为了探究内部工作流程的差异。

2.1、简单请求

2.1.1 什么做是「简单请求」

只要同时满足以下两个条件的请求,就属于「简单请求」

  1. 请求方法属于以下三者之一

    • HEAD(获取资源的元信息)
    • GET
    • POST
  2. 请求头字段在以下范围之内

    • Accept(用来告知(服务器)客户端可以处理的内容类型,MIME格式)

    • Accept-Language

    • Content-Language

    • Content-Type(在请求头中,用来告诉服务器实际发送的数据类型),只限三个值范围之内:

      • application/x-www-form-urlencoded(表单默认的提交数据格式)
      • multipart/form-data(需要在表单中进行文件上传时,使用该格式)
      • text/plain(纯文本格式)

无法同时满足条件1、2的,就属于「非简单请求」。

这样划分的原因是,以上的「简单请求」就和表单提交一样。而早期的HTML表单可以向任何来源提交简单请求,所以编写服务器的人一定对此进行了网络攻击防护。基于这样的假设,CORS不对看起来像表单提交的「简单请求」做过多额外的处理(不使用OPTIONS预检流程)。但是!服务器响应中必须提供Access-Control-Allow-Origin字段,以让浏览器判断是否同意解析响应内容。

2.1.2 origin发挥了什么作用

在前面的介绍中提到,origin字段是为了标识 跨域请求发起的「域」。但这个「标识」,究竟是在哪里发挥作用的?服务端?浏览器中?

对于这个问题,结合我的个人理解,大概可以有以下两种答案:

  • 第一种,origin在服务端发挥作用。服务端获取 request header 中的origin字段,然后判断是否在允许的范围内。如果允许,就返回Access-Control-Allow-Origin等字段;如果不允许,就不返回 CORS 相关的字段。

    如果是这种解释,那么Access-Control-*-*系列字段就失去存在的理由了,最起码Access-Control-Allow-Origin字段就完全没有必要发送给浏览器。

  • 第二种,origin在浏览器中发挥作用,浏览器根据服务端返回的响应头中的 Access-Control-Allow-Origin字段,与自身请求头中的origin字段进行比较,判断是否匹配。如果没有匹配上,浏览器终止后续的CORS流程;如果匹配上了,浏览器继续执行后续的CORS流程:简单请求继续解析响应内容,非简单请求继续发送请求。

    这种解释与自己实际动手实验所观察到的情况比较符合,不过也存在让人感觉很不合理的地方:既然origin字段并没有在服务端发挥作用,为什么还要添加到请求头中,发送到服务端呢?

因为对于这个疑问的回答,直接左右了服务端的处理逻辑,所以在开始正式探究CORS流程前,得先搞清楚这个问题!

查询了CORS - W3C Wiki、以及搜索引擎查询了该问题,均未找到答案。同时,第二种解释更接近于实际,所以下面的探讨基于第二种解释展开。

2.1.3 「简单请求」的CORS流程

  • 发送实际请求
  • 浏览器根据响应头的CORS相关字段,决定是否解析响应

对于简单请求,浏览器直接发送(GET、POST或HEAD)请求,同时自动在请求头中添加origin字段。该字段表示网页的域(协议+域名+端口,但不包括url中的路径和端口)

服务端返回正常的响应(status:200),并带上Access-Control-*-*响应头字段,如Access-Control-Allow-Origin等,浏览器比较Access-Control-Allow-Originorigin是否匹配。如果不匹配,不解析响应的内容;如果匹配,解析响应的内容。

2.2、非简单请求

2.2.1 什么是「非简单请求」

不是「简单请求」的请求

2.2.2 「非简单请求」的CORS流程

  • 第一步,发送「预检请求」
  • 第二步,根据「预检请求」的结果,决定是否继续发送实际请求

当浏览器检测到请求为跨域请求后,并且属于「非简单请求」时,浏览器会首先发送一个OPTIONS请求到服务器,以获知服务器是否允许该实际请求,

服务器会返回Access-Control-*-*字段,浏览器根据这些字段,判断该实际请求是否符合条件。如果不符合条件,就不发送实际请求,如下图:

image-20230307204903208

没有通过「预检请求」,实际请求不会被发送

如果符合条件,浏览器将继续发送实际请求,然后得到响应。如下图:

image-20230307205247585

image-20230307205303726

上图:浏览器发送OPTIONS预检请求,下图:通过预检请求,继续发送实际请求



三、如何配置CORS

3.1、CORS控制字段

CORS,新增了一组HTTP标头字段,来允许服务器声明哪些源站通过浏览器有权限访问哪些资源

  1. Access-Control-Allow-Origin,指定允许的源

    取值范围<orgin> | *,其中<origin>表示具体的源,*是通配符,表示所有源

  2. Access-Control-Allow-Methods,指定访问资源时允许使用的请求方法

    示例:Access-Control-Allow-Methods: POST, GET, OPTIONS

  3. Access-Control-Allow-Headers,指定实际请求中允许携带的请求头字段

    示例:Access-Control-Allow-Headers: Content-Type

  4. Access-Control-Allow-Credentials,指定当浏览器的credentials 设置为 true 时是否允许浏览器读取 response 的内容(比如cookie)

  5. Access-Control-Max-Age,指定预检请求的结果能够被缓存多久,单位秒

    示例:Access-Control-Max-Age: 60



3.2、Request头(延伸)

可用于发起跨源请求的标头字段。请注意,这些标头字段无须手动设置。

  1. Origin,跨源请求的源站,值为源站的URL(不包含任何路径信息)
  2. Access-Control-Request-Method,用于预检请求,表示实际请求时将使用的HTTP方法
  3. Access-Control-Request-Header,用于预检请求,表示实际请求时将要携带的标头字段




四、CORS配置示例

下面是Express项目中设置的CORS规则,操作很简单(当然我也不知道这样是否符合规范),就是在返回的响应头中加入Access-Control-*-*字段,声明允许跨域的范围。剩下的操作全部由浏览器执行。

// app.js
...
​
// 设置CORS跨域资源共享,all匹配所有的HTTP动作
app.all('*',(req,res,next)=>{
  res.header("Access-Control-Allow-Origin", 'http://localhost:5173'); // *表示任意域名下的页面都可以请求这台服务器
  // res.header("Access-Control-Allow-Origin", "https://www.timegogo.top");  // 表示指定域名下的网页可以请求这台服务器
​
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS') // 表示何种HTTP请求方式可以请求这台服务器
  res.header('Access-Control-Content-Type', 'application/json')
  res.header('Access-Control-Allow-Headers', 'Content-Type')  //这个响应头必须显示设置,不然无法在给跨域请求的响应中加入头
  next()
})
​
...




五、配置CORS需要注意

CORS如果配置不当,将会造成安全漏洞,如CSRF跨站请求伪造,窃取隐私信息。

这里不展开描述 CORS 配置不当是如何引起安全漏洞的。如果想要了解更多,请自行阅读:CORS跨域漏洞学习 。这里直接给出结论:配置 CORS 时需要注意哪些问题:

  1. 如果Web资源包含敏感信息,则应在Access-Control-Allow-Origin标头中正确指定来源,而不是使用通配符*

  2. 避免将null加入白名单

    避免使用标题Access-Control-Allow-Origin: null。来自内部文档和沙盒请求的跨域资源调用可以指定null来源

  3. CORS不能代替服务器安全策略

    除了正确配置的CORS之外,Web服务器还应继续对敏感数据应用保护,例如身份验证和会话管理





参考文章

跨源资源共享(CORS) - HTTP | MDN (mozilla.org)

CORS跨域漏洞学习 - Lushun - 博客园 (cnblogs.com)