浅谈 Cross-Origin Resource Sharing(CORS)

626 阅读9分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

日常开发过程中遇到的CORS

日常开发过程中,最最最常见的问题就是,当兴致勃勃的首次发布一个网站到互联网后。打开网站发现网站显示异常,有经验的你打开devTools中的日志输出,CORS错误赫然显示在控制台上。如下图所示。

cors-01.png 此时我们仔细看红色的报错信息,可以很直白的明白,客户端告诉你「服务器没有配置允许跨源请求的请求源(Access-Control-Allow-Origin),所以这个资源无法发起请求」。

此时,引出一个问题,为什么会报这个错?这个错需要在哪里进行修复?以下我们一一分析。

什么是CORS

CORS(Cross-Origin Resource Sharing) 跨源资源共享;个人理解,CORS就是客户端从源A向源B请求资源并在源A使用的过程。

在浏览器中,如果是跨源请求资源,每次一次针对一个资源的请求,客户端都会由【preflight + 实际请求】的组合来完成。如下图所示。

cors-02.png

可以看到,在no-cache.css 资源的请求过程中,同时存在preflight + fetch的组合请求。通常preflight的 method 为options 如图 2-3 所示,fetch的 method 为实际请求时的定义 如图 2-4 所示。

http-cache-02转存失败,建议直接上传图片文件 图2-3
http-cache-03转存失败,建议直接上传图片文件 图2-4

CORS错误应该如何修复及其原理

原理如下图所示

从流程图中可以看到,从客户端发起跨源请求,到客户端接受服务器的响应,中间是需要服务端做CORS配置的。什么意思呢?

  • 客户端从源A向源B发起资源请求,客户端会判断,是否为简单请求
    • 如果是简单请求,还会检验是否使用了自定义header(如Cache-Control、自定义的X-AAA-BBB),如果存在则会发起preflight。如果不存在,则直接向服务器发起请求。
    • 如果不是简单请求,则一定会向服务器发起preflight
  • preflight中,request header会携带如下字段
    • origin: 表示请求的源,其值为源A的地址。
    • access-control-request-method: 表示请求的方法。
    • access-control-request-header: 表示请求中的自定义头部
  • 在服务器接收到preflight后,会在response header中返回服务器配置的cors信息
    • Access-Control-Allow-Origin: 表示允许请求的源。一般设置为 *
    • Access-Control-Allow-Method: 表示允许请求的方法。一般设置为GET,HEAD,PUT,PATCH,POST,DELETE
    • Access-Control-Allow-Headers: 表示允许请求的自定义头部。一般设置为access-control-request-header中携带的值。
  • 客户端在接收到服务器的配置后,会匹配服务端的cors配置。如果同时满足originmethodheaders的条件,则客户端会发起真正的请求。否则客户端会报CORS错误。
  • 客户端发起真正的请求后,客户端就会接收到源B发送过来的跨源资源,并在客户端展示。

所以,客户端CORS错误的最终解决方式,还是需要服务端进行CORS配置。

如果留心,可以看到流程图中,有一个标注“预检请求响应到真正发起请求之间,可能会存在阻塞”,这个问题请看 什么是 preflight 小节.

延伸思考

什么是简单请求

简单请求是在跨域资源共享(CORS)机制下的一种请求类型。在浏览器中,由于同源策略的限制,跨域请求(即从一个域名的网页向另一个域名的服务器请求资源)通常是被禁止的。但是,CORS定义了一种机制,允许服务器声明哪些源(域、协议、端口的组合)有权访问其资源。

简单请求是一种简单的 HTTP 请求,它满足了一系列条件,包括:

  1. 使用以下 HTTP 方法之一:GET、HEAD、POST。
  2. 仅使用以下标准的请求标头之一:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain)。
  3. 使用了简单的标头。这些标头包括:Accept、Accept-Language、Content-Language、Content-Type(仅限于上述类型)、DPR、Downlink、Save-Data、Viewport-Width、Width。
  4. 请求中的任何 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可通过 XMLHttpRequest.upload 属性访问。

如果请求满足这些条件,则浏览器会自动执行跨域请求,而无需发送预检请求(OPTIONS 请求)。这使得跨域请求的处理更加高效。

什么是preflight

preflight 即 预检请求。

简单理解:在跨源资源共享(跨域请求)的场景下,客户端在发送请求前会先发送一个预检请求,询问服务器当前请求源(origin) & 请求方式 (method) & request携带的自定义请求头 (headers) 是否允许跨源请求。服务器如果允许,则服务器会在response中返回对应头,浏览器收到后根据response头发送请求 或 报cors错误。

preflight阻塞

为方便测试,强制在服务侧为每一个请求都增加了500ms的延迟,用于模拟服务器响应等待时间

现象

cors-02.png

如图所示,从waterfull中可以看到,在发起对no-cache.css的请求时,preflight请求是率先执行的,此时客户端等待预检请求的结果为500ms。而后再开始执行no-cache.css的资源请求,客户端等待资源响应又耗时500ms。由此而来,从真正发起资源请求,到真正的响应与渲染,在网络请求中总共耗时了1000ms。即使预检请求通过后,客户端读取的是缓存数据。也会耗时500ms来处理预检请求,如下图所示。

cors-06.png

造成这样的原因
  • preflight请求,同样需要走服务端验证,等待时间就是服务端响应时间

试想下,如果此时多链路并发请求,那么最后从开始请求到最后一个请求响应需要多久呢??

解决方案
  • 以node + express为例。服务端在cors配置中,增加缓存配置。从而在下一次请求时,客户端的预检请求走缓存。

cors-07.png

  • 多请求合并为一个,减少开销

  • 优化服务端性能

配置preflight缓存的优缺点

优点

  • 对于正常的非定制化响应的系统(如大型资源管理系统或权限系统等)的资源请求来说,增加preflight缓存时间,有效的减少了服务开销和数据响应等待时间。
  • preflight 缓存可减少客户端交互时间,提升用户体验
  • 大量减少服务器压力

缺点

  • 对于定制化响应的系统的资源请求来说,可能包含的资源访问权限、资源可读可写等功能,如果涉及缓存,那么可能在配置修改后,不会第一时间生效。需要等待缓存过期,可能会造成某些歧义。

fetch是如何发起跨源请求的

控制fetch是否进行跨域请求的关键参数mode

参数名称参数含义浏览器会做什么
cors允许跨源请求,但是需要服务端配置CORS在这种模式下,浏览器会在请求头中添加 Origin 头部,并期望在响应头中看到
Access-Control-Allow-Origin 头部。如果服务器不返回这个头部,或者这个头部
的值不包含请求的源,那么浏览器会拒绝这个请求。
no-cors允许跨源请求,但不需要服务端支持CORS在这种模式下,浏览器不会在请求头中添加 Origin 头部,也不会检查响应头中的
Access-Control-Allow-Origin 头部。但是,这种模式下的请求有很多限制,例如,
你不能设置自定义的请求头部,只可以使固定的头信息,也不能读取响应的内容。这种模式
要用于加载CORS 不安全的资源,如图片或脚本。
same-origin只有在同源的情况下,才会发送请求。如果请求的资源在不同的源,请求会被拒绝。
这是为了保证安全性,防止从恶意网站发起的跨站请求。

示例

  • 使用mode: cors

cors-08.png

可以看到,预检请求中,Request HeaderSec-Fetch-Mode值为cors,且客户端也向服务端发送了Origin:http://localhost:63342,服务端响应的Access-Control-Allow-Origin 也包含http://localhost:63342。此时客户端可以向服务端发起资源获取请求。

  • 使用no-cors

cors-09.png 可以看到,预检请求中,Request HeaderSec-Fetch-Mode值为no-cors,但是客户却没有向服务器发起预检请求,而是直接发起请求。

  • 使用same-origin

cors-10.png

可以看到,在非同源的情况下,请求资源,客户端会报错,告诉用户当前请求的资源与服务器不是同一个ip。

使用场景:

  • 内部api调用:前端调用后端的API在同源上,可以使用 same-origin 模式来确保这些请求不会被意外地发送到其他域。
  • 用户认证与授权:在处理用户登录、登出和获取用户信息等涉及敏感数据的操作时,使用 same-origin 模式可以增加安全性。
  • 数据保护:确保财务数据、个人信息等敏感数据只在同源环境下传输。
  • 单页应用:在单页应用中,所有的 API 请求都通常在同一个域下运行,因此使用 same-origin 模式可以确保应用的安全性。

示例代码 github.com/SecretCastl…