不是全部的 No 'Access-Control-Allow-Origin' header... 都是跨域问题 - 记一次图片上传踩坑

什么是跨域 ?

跨域这个问题你们并不陌生,这也是面试的高频问题,不少人都背过,什么由于同源策略啊,CORS 啊等等,跨域的标致就是浏览器控制台出现 Access to XMLHttpRequest at 'https://xxx.xxx.com' from origin 'https://xxx.xxx.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.html

首先,只有 Web 浏览器才会产生跨域,这是由于浏览器的同源策略在限制。同源策略就是 [域名(又称为主机 host),端口(port),协议(protocol)] 要统一,不然就会被浏览器断定为跨域,而后拒绝请求。可是严格的说,浏览器并非拒绝全部的跨域请求,实际上拒绝的是跨域的读操做。前端

同源策略并非很差,它在必定程度上保证了浏览器的安全,只是没法适应现代的潮流,在工程服务化后,不一样职责的服务分散在不一样的工程中,每每这些工程的域名是不一样的,但一个需求可能须要对应到多个服务,这时便须要调用不一样服务的接口。node

哪些状况下会产生跨域 ?

URL                             说明                是否容许通讯
http://www.a.com/a.js
http://www.a.com/b.js           同一域名下            容许

http://www.a.com/lab/a.js
http://www.a.com/script/b.js    同一域名下不一样文件夹    容许

http://www.a.com:8000/a.js
http://www.a.com/b.js           同一域名,不一样端口      不容许

http://www.a.com/a.js
https://www.a.com/b.js          同一域名,不一样协议      不容许

http://www.a.com/a.js
http://70.32.92.74/b.js         域名和域名对应ip        不容许

http://www.a.com/a.js
http://script.a.com/b.js        主域相同,子域不一样      不容许(cookie这种状况下也不容许访问)

http://www.a.com/a.js
http://a.com/b.js               同一域名,不一样二级域名(同上) 不容许(cookie这种状况下也不容许访问)

http://www.cnblogs.com/a.js
http://www.a.com/b.js           不一样域名            不容许
复制代码

什么是 CORS

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不一样源服务器上的指定的资源。当一个资源从与该资源自己所在的服务器不一样的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 好比,站点 http://domain-a.com 的某 HTML 页面经过 <img> 的 src 请求 http://domain-b.com/image.jpg。网络上的许多页面都会加载来自不一样域的 CSS 样式表,图像和脚本等资源。
出于安全缘由,浏览器限制从脚本内发起的跨源 HTTP 请求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。ios

跨域资源共享( CORS )机制容许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以下降跨域 HTTP 请求所带来的风险。nginx

不过呢,并不必定是浏览器限制了发起跨站请求,也多是跨站请求能够正常发起,可是返回结果被浏览器拦截了。面试

在“上古时代”,解决跨域有不少黑科技,什么 JSONP 啊,window.name 啊,document.domain 啊等等都用上了,但现代用的基本都是CORS跨域,因此上述方法在这里就不讨论了。后端

由上述可知,实现 CORS 通讯的关键是服务器。只要服务器实现了 CORS 接口,就能够跨源通讯。跨域

虽然工做重心在后端,可是做为前端,也要了解这方面的知识,不然就会出现204预请求,而后后端没处理,就甩锅给前端的状况。浏览器

下面介绍一些 CORS 过程当中常见的 HTTP 响应头(Response Headers)。安全

什么是 Access-Control-Allow-Origin

这是 HTTP 响应首部中的一个字段,

具体格式是: Access-Control-Allow-Origin: <origin> | *

其中,origin 参数的值指定了容许访问该资源的外域 URI。对于不须要携带身份凭证的请求,服务器能够指定该字段的值为通配符,表示容许来自全部域的请求。

若是服务端指定了具体的域名而非*,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不一样的源站返回不一样的内容。例如:

// 只响应来自 http://mozilla.com 的请求
Access-Control-Allow-Origin: http://mozilla.com
复制代码

什么是 Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,代表服务器支持的全部跨域请求的方法。注意,返回的是全部支持的方法,而不单是浏览器请求的那个方法。这是为了不屡次"预检"请求。

具体格式是:Access-Control-Allow-Methods: <method>[, <method>]*

什么是 Access-Control-Allow-Headers ?

可支持的请求首部名字。请求头会列出全部支持的首部列表,用逗号隔开。

若是浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,代表服务器支持的全部头信息字段,不限于浏览器在"预检"中请求的字段。

具体格式是:Access-Control-Allow-Headers: <header-name>[, <header-name>]*

什么是 Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否容许发送 Cookie。默认状况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 能够包含在请求中,一块儿发给服务器。

浏览器的正常请求和回应

一旦服务器经过了"预检"请求,之后每次浏览器正常的 CORS 请求,就都跟简单请求同样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

好了,铺垫完成,接下来要说说我踩的坑了。

问题实记

项目背景

公司因历史遗留,同时存在3种后端语言:Java,PHP,Node.js,由于后端同事都不懂 Node.js,因此 Node 项目一直是前端维护。老项目用的是 Koa 框架,以前我在上面写逻辑,上传图片是没有问题的,直到我用了 Nest 框架重构,将后台管理系统的逻辑拆分解耦出来。

踩坑过程

新项目一切请求都正常,本地跑的时候也正常,可是到了线上,只有上传图片不正常。每次上传都会预请求一次 204 OPTIONS,这一步没问题,但接下来的 POST 请求就有问题了:

我一看控制台的信息,结合多年的开发经验(其实并无),这不就是跨域嘛,因而看代码,main.ts 中已经有了 app.enableCors(),百思不得其解,因而 Google,发现也有人遇到过相似的问题,说是 Nest 某些版本在线上环境没法正确使用 CORS,而后就造成了下列代码:

可是,并无什么卵用,调试了好一下子才注意到响应头是 nginx 返回的,而后就像发现新大陆同样屁颠屁颠的找运维老大:代码里面已经设置了 CORS 跨域了,是否是被 nginx 拦截了?(公司的服务器经过 nginx 作负载均衡,而后才到 node)

而后运维老大很配合的帮我配置了 nginx 的跨域,而后就悲剧了,连 OPTIONS 都过不去😂:

控制台的大体意思是 CORS 规则冲突了,只能使用一种。

因此这个问题其实和 nginx 没有什么关系,运维大佬看了访问日志,说是 OPTIONS 请求是响应了的,可是到 POST 请求的时候就断开链接了,而后让我试试直接访问端口,因而控制台又出现了以下信息:

唔,大体意思是,源头是 https 协议的话,就不能请求 http 协议的资源。

因而我把网站上的 https 换成 http,就出现了以下信息:

我就以为很奇怪,由于本地开发的时候,是能正常上传图片的。惟一不一样的地方就在于,线上使用的是 pm2 进程管理工具,而我本地用的是自带的 nodemon。为了验证个人猜想,因而本身的电脑上也装了 pm2,而后跑起来,而后就。。。Bug 果真复现了。

因而让运维大佬不用 pm2 直接用 nodemon 启动试试,而后折腾我好久的跨域问题就“解决了”。为何打引号呢,由于运维大佬并非很想用 nodemon 来管理进程,主要是若是服务器宕机,不能自动拉起,战役还远没有结束。

而后我就围绕着 pm2 继续探索,把官方文档看了个遍,也没找到关键点,郁闷之下,只好检查的 pm2 启动配置,而后注意到这个:

这是当初我为了输出日志的时候,更新了文件,防止被 pm2 监听到,致使服务一直重启所作的措施。而后就想到了,我上传图片的时候,是先在硬盘保存,而后读取 Buffer 流,而后再上传到 oss,最后删掉硬盘的图片,核心代码以下:

因此我就在想,是否是由于上传的时候,存本地的图片触发了 pm2 的监听,致使服务重启,因此就会报 net::ERR_CONNECTION_RESET,因而我改为了临时目录 const uploadCachePath = '/tmp/assets/uploads'; (Mac OS、Linux 都有这个目录),而后 pm2 启动,上传,成功。

至此,折磨了我近一周的 Bug 终于修复,结果和跨域没有半毛钱关系。

插曲

有读者可能注意到 /tmp/assets/uploads 路径,要是同事用的是 Windows 系统开发咋办?这个我天然也想到了,因而改了 pm2 的启动项:

但是不管怎样改,依然会触发上述 Bug,由于服务器同时跑着 2 个 Node 项目,另外一个项目也有本身的 pm2 启动项,因此感受这个配置被另外一个覆盖了,pm2 彷佛是全局的。若是有其余大神深刻了解过 pm2 的能够指点一下。

因此和运维大佬讨论了一下,给我开了权限,就暂时用这个临时目录,待之后找到更好的解决方案再优化,反正目前这个项目也只有我一人在维护。

遇到的其余场景

1. 服务器宕机

就在我刚找到解决方案的时候,我带的小弟跑过来问我是否是动了配置文件,老项目怎么都跨域了。我去看他的控制台,确实有 Access to ... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 的信息,因为刚踩的坑,况且我也没动过配置文件,因此以为这确定不是跨域问题。而后我看了服务器的日志,发现一直在重启,由于有个模块他没同步上去,只同步了路由文件,致使路由找不到对应的函数,服务就一直在报错重启,根本就没处理请求。因而让他把代码从新上传,问题解决。

因此我的猜想(由于没有权限看服务器的配置),此次跨域是 nginx 代理的时候,由于访问不到 Node 服务,因此天然而然地就读不到 CORS 配置,而后报跨域错误。

2. Axios 的自行检查

Axios 建立实例时,有个字段须要注意:

若是不设为 false,则会获得下面报错:

缘由就是前面提到的 Access-Control-Allow-Credentials 字段,CORS 请求默认不发送 Cookie 和 HTTP 认证信息。若是要把 Cookie 发到服务器,一方面要服务器赞成:

Access-Control-Allow-Credentials: true

另外一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性,也就是 Axios 默认打开的 withCredentials: true

不然,即便服务器赞成发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

可是,若是省略withCredentials设置,有的浏览器仍是会一块儿发送 Cookie。这时,须要显示关闭 withCredentials: false

须要注意的是,若是要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其余域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也没法读取服务器域名下的 Cookie。

总结

由上述能够总结,在后端配置了 CORS 的状况下,还会形成 Access to ... has been blocked by CORS policy ... 的状况大体有:

  1. 服务器忽然重启,致使代理服务器转发中断;
  2. 服务器宕机,致使代理服务器读不到 CORS 配置;
  3. Axios 等请求插件设置了withCredentials: true,致使先行验证并拦截了请求;

有些时候,浏览器控制台给出的错误信息,不必定能真正地指出问题的所在,作为前端,还须要多去了解一些更本质的东西。

参考资料

《跨域资源共享 CORS 详解》- 阮一峰的网络日志

《前端跨域整理》

《HTTP 权威指南 - O'REILLY》

·

相关文章
相关标签/搜索