本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
日常开发过程中遇到的CORS
日常开发过程中,最最最常见的问题就是,当兴致勃勃的首次发布一个网站到互联网后。打开网站发现网站显示异常,有经验的你打开devTools中的日志输出,CORS错误赫然显示在控制台上。如下图所示。
此时我们仔细看红色的报错信息,可以很直白的明白,客户端告诉你「服务器没有配置允许跨源请求的请求源(Access-Control-Allow-Origin),所以这个资源无法发起请求」。
此时,引出一个问题,为什么会报这个错?这个错需要在哪里进行修复?以下我们一一分析。
什么是CORS
CORS(Cross-Origin Resource Sharing) 跨源资源共享;个人理解,CORS就是客户端从源A向源B请求资源并在源A使用的过程。
在浏览器中,如果是跨源请求资源,每次一次针对一个资源的请求,客户端都会由【preflight + 实际请求】的组合来完成。如下图所示。
可以看到,在no-cache.css 资源的请求过程中,同时存在preflight + fetch的组合请求。通常preflight的 method 为options 如图 2-3 所示,fetch的 method 为实际请求时的定义 如图 2-4 所示。
图2-3
图2-4
CORS错误应该如何修复及其原理
原理如下图所示
从流程图中可以看到,从客户端发起跨源请求,到客户端接受服务器的响应,中间是需要服务端做CORS配置的。什么意思呢?
- 客户端从源A向源B发起资源请求,客户端会判断,是否为简单请求。
- 如果是简单请求,还会检验是否使用了自定义header(如Cache-Control、自定义的X-AAA-BBB),如果存在则会发起
preflight。如果不存在,则直接向服务器发起请求。 - 如果不是简单请求,则一定会向服务器发起
preflight
- 如果是简单请求,还会检验是否使用了自定义header(如Cache-Control、自定义的X-AAA-BBB),如果存在则会发起
- 在
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,DELETEAccess-Control-Allow-Headers: 表示允许请求的自定义头部。一般设置为access-control-request-header中携带的值。
- 客户端在接收到服务器的配置后,会匹配服务端的cors配置。如果同时满足
origin、method、headers的条件,则客户端会发起真正的请求。否则客户端会报CORS错误。 - 客户端发起真正的请求后,客户端就会接收到源B发送过来的跨源资源,并在客户端展示。
所以,客户端CORS错误的最终解决方式,还是需要服务端进行CORS配置。
如果留心,可以看到流程图中,有一个标注“预检请求响应到真正发起请求之间,可能会存在阻塞”,这个问题请看 什么是 preflight 小节.
延伸思考
什么是简单请求
简单请求是在跨域资源共享(CORS)机制下的一种请求类型。在浏览器中,由于同源策略的限制,跨域请求(即从一个域名的网页向另一个域名的服务器请求资源)通常是被禁止的。但是,CORS定义了一种机制,允许服务器声明哪些源(域、协议、端口的组合)有权访问其资源。
简单请求是一种简单的 HTTP 请求,它满足了一系列条件,包括:
- 使用以下 HTTP 方法之一:GET、HEAD、POST。
- 仅使用以下标准的请求标头之一:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain)。
- 使用了简单的标头。这些标头包括:Accept、Accept-Language、Content-Language、Content-Type(仅限于上述类型)、DPR、Downlink、Save-Data、Viewport-Width、Width。
- 请求中的任何 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可通过 XMLHttpRequest.upload 属性访问。
如果请求满足这些条件,则浏览器会自动执行跨域请求,而无需发送预检请求(OPTIONS 请求)。这使得跨域请求的处理更加高效。
什么是preflight
preflight 即 预检请求。
简单理解:在跨源资源共享(跨域请求)的场景下,客户端在发送请求前会先发送一个预检请求,询问服务器当前请求源(origin) & 请求方式 (method) & request携带的自定义请求头 (headers) 是否允许跨源请求。服务器如果允许,则服务器会在response中返回对应头,浏览器收到后根据response头发送请求 或 报cors错误。
preflight阻塞
为方便测试,强制在服务侧为每一个请求都增加了
500ms的延迟,用于模拟服务器响应等待时间
现象
如图所示,从waterfull中可以看到,在发起对no-cache.css的请求时,preflight请求是率先执行的,此时客户端等待预检请求的结果为500ms。而后再开始执行no-cache.css的资源请求,客户端等待资源响应又耗时500ms。由此而来,从真正发起资源请求,到真正的响应与渲染,在网络请求中总共耗时了1000ms。即使预检请求通过后,客户端读取的是缓存数据。也会耗时500ms来处理预检请求,如下图所示。
造成这样的原因
- preflight请求,同样需要走服务端验证,等待时间就是服务端响应时间
试想下,如果此时多链路并发请求,那么最后从开始请求到最后一个请求响应需要多久呢??
解决方案
- 以node + express为例。服务端在cors配置中,增加缓存配置。从而在下一次请求时,客户端的预检请求走缓存。
-
多请求合并为一个,减少开销
-
优化服务端性能
配置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
可以看到,预检请求中,Request Header中Sec-Fetch-Mode值为cors,且客户端也向服务端发送了Origin:http://localhost:63342,服务端响应的Access-Control-Allow-Origin 也包含http://localhost:63342。此时客户端可以向服务端发起资源获取请求。
- 使用
no-cors
可以看到,预检请求中,
Request Header中Sec-Fetch-Mode值为no-cors,但是客户却没有向服务器发起预检请求,而是直接发起请求。
- 使用
same-origin
可以看到,在非同源的情况下,请求资源,客户端会报错,告诉用户当前请求的资源与服务器不是同一个ip。
使用场景:
- 内部api调用:前端调用后端的API在同源上,可以使用
same-origin模式来确保这些请求不会被意外地发送到其他域。 - 用户认证与授权:在处理用户登录、登出和获取用户信息等涉及敏感数据的操作时,使用
same-origin模式可以增加安全性。 - 数据保护:确保财务数据、个人信息等敏感数据只在同源环境下传输。
- 单页应用:在单页应用中,所有的 API 请求都通常在同一个域下运行,因此使用
same-origin模式可以确保应用的安全性。