双17期|跨域资源共享:跨域错误该前端还是后端处理?


theme: channing-cyan

highlight: a11y-dark

双17期|跨域资源共享:跨域错误该前端还是后端处理?

tips:每个技术点都值得优学优写:17期

一、写在前面

导读:本文旨在讲清楚跨域资源共享(CORS)的由来,CORS 的一些限制,以及解除这些限制的方法。不重在提供更多的跨域解决方案,尽管一些同学更喜欢上来就要答案,而不是看推演和分析。当然文末也附加了一些跨域解决方案。

XHR 是一个浏览器层面的的 API,我们使用起来很简便,但其实它的内部隐藏了大量的底层处理,例如重定向、缓存、认证相关,以及内容协商等,算是内部高度封装隐藏,对外暴露少量的一种情况。

当然,也正因如此,使得:

  • ①XHR的API使用变得更简易,人们能够把更多的精力放在业务实现上。

  • ②这样使得应用代码得到了浏览器沙箱机制施加的安全保护,这种保护体现在安全限制上。

浏览器沙箱中,有一种情形是同源沙箱,它不允许 hello.com 中脚本访问操作 word.com 中的用户数据。

事实上,早起的 XHR 都限制应用只能执行同源请求,就说是在 hello.com 中只能访问请求 hello.com 中的资源,如果不同源,浏览器就会拒绝该 XHR 请求并报错。

为什么不提 axios,ajax 等支持异步请求的插件,老提已经很少用的 XHR,XHR 并没有过时,也不是很少被使用, 只是不在明面而已,而且早期那时还没 axios,ajax 啥事,并且这两的本质也都是 XHR,看下浏览器的控制,可以感受下 XHR 和 axios,ajax的关系。当你发起一个 axios 请求时,你想看它的请求情况,点开选项是 XHR 还是啥呢?

image.png

二、同源与跨域资源共享(CORS)

  • ①什么是同源

此时,我们引入了一个同源的概念,那么什么是“源”呢?清楚“源”的组成成分,才能很好的区分是否同源。

一个“源”由于应用协议、域名、和端口号共同组成。所以,只有这三者都一样才能算是同源,三者中只要有一方不同,都是非同源。

  • ②为什么要有同源策略?

同源策略的出发点:一些敏感的数据被浏览器存储,例如用户相关数据、认证令牌、cookie等。这些数据不应被泄露给其他应用(其他源),同源策略正是处于保护数据不被泄露的目的而设置的。它禁止了不同源的脚本相互访问和操作数据。

  • ③同源策略的限制和跨域资源共享

    同源策略提供了安全保护,但有时也会带来麻烦,因为同源策略限制了不同源之间资源的置换。这就引出了跨域资源共享(Cross-Origin Resource Sharing,CORS)。CORS 策略使得不同源之间发起请求得以实现,但是 CORS 设置前置条件:选择同意机制。当然这个选择同意机制由浏览器底层处理。

该选择同意机制的处理逻辑大概是:浏览器对发起的请求,自动追加包含着请求来源的受保护的 Origin HTTP 首部,而服务器具有检查请求的 Origin 首部,选择是否接收该请求的权利,同意接收请求则返回 Access-Control-Allow-Origin 相应首部,不同意则在响应中不包含 Access-Control-Allow-Origin 相应首部即可。服务器同意之后就实现了跨域资源共享,所以解决跨域问题,实现跨域资源共享的常规办法之一,就是服务端的同学对跨域请求设置同意,响应中设置 Access-Control-Allow-Origin 相应首部。

如果你在使用一些异步请求时足够仔细,那么可能会发现下面的信息:

image.png

tips:正如上图所示,CORS 允许服务器设置 Access-Control-Allow-Origin:*,这个 * 是一个通配值,表示允> 许和同意接收来自任何“源”的请求。这种写法应该谨慎使用,更合适的写法应该是写法具体的“源”,例如: Access-Control-Allow-Origin:http:hello.com

  • ④跨域资源共享的其他内容

CORS 为确保服务器支持它,会采取一些列措施,例如:

①CORS 请求默认会省略 HTTP 认证和 cookie 等用户凭证。

②浏览器会限制 CORS 请求只能发送 head、get、post特定方法的请求。(所以注意的话,有时可能会遇到 put 请求会触发跨域报错,而一些 get请求则不会,这在 ie 中可能更能体现。)

③只能访问可以通过 XHR 发送并读取的 HTTP 首部。

那么如何让 CORS 请求支持 cookie 和 HTTP 认证呢?

回答:客户端在发送异步请求时,设置 withCredentials 为 true。意味跨域请求是否使用凭证。同时服务器也必须应向适当的首部(Access-Control-Allow-Credentials),意味同意使用凭证,允许应用程序发送用户的隐私数据。所以一些同学可能会发现 axios 要想支持丰富的跨域请求就得设置 withCredentials。就像下面这样:

js // 创建axios实例 const service = axios.create({ headers: { 'token': '' }, baseURL: '', withCredentials: true // 表示跨域请求时是否需要使用凭证,开启后,后端服务器要设置允许开启 })

需要注意的是前端应用设置了 withCredentials,后端接口需要配合设置 Access-Control-Allow-Credentials 才有效。

那么如何让 CORS 请求支持客户端写或者读自定义的 HTTP 首部,或者支持使用 delete、put 等“不简单”的请求方法?

回答:所以有时会发现,明明浏览器控制台能看到响应中包含有自定义的 HTTP 首部(请求头字段),但是前端却读取不到,这都是 CORS 的选择同意机制的限制。要想实现这些,需要服务端的配合。

默认 reponse header 只能取到 Content-Language,Content-Type,Expires,Last-Modified,Pragma ,5个默认值,要想取得其他的自定义字段需要在服务端设置 Access-Control-Expose-Headers,配置前端想要获取的 header,比如自定义的 filename,需要在后端做类似下面这样的设置。

image.png

还有一点就是需要向服务器发送预备(preflight)请求。当然这一步通常是底层处理,不需要程序员处理。

三、写在后面

tips ①:W3C 的 CORS 规范规定了:“简单的”请求可以跳过发送预备(preflight)请求,但其他“不简单的”请求都需要发送预备请求。毫无疑问这种预备请求验证会带来延迟问题,好在是完成预备请求,浏览器就会将结果缓存起来,后续请求就不必重复验证了。

tips ②:了解更多的 CORS 策略和实现,请参考 W3C 官方标准。

tips ③:所以,跨域问题该由前端还是后端处理?看懂 CORS 的选择同意机制,应该都知道答案了,那就是需要前后端共同处理或都可以处理, 没有限定一方。是协同处理还是一方处理,要看选用的解决方案,比如常规解决跨域资源共享,那就得前后端协同配合, 如果另辟蹊径对浏览器规避“非同源”和 XHR,例如利用 nginx 反向代理那就是前后端都可以,看谁管理 nginx 了。

实践中有一些办法可以“欺骗”浏览器,“欺骗”浏览器没有执行非同源的请求(跨域请求),尽管在结果上就是进行了跨域资源共享。 但是由于成功“欺骗”了浏览器,导致没触发 CORS 相关的策略,比如借助 nginx 的代理转发功能,把跨源这一步转移到了 nginx 中, 在浏览器端表现的是同源请求。这种手段另辟蹊径,绕过了 CORS 的选择同意机制,因为浏览器认为那是同源请求, 他们把跨源这一步移交给了 nginx 处理。vue-cli 中的 proxy 也是这个道理吧?

tips ④:注意,nginx 没有 CORS 策略,同样的 postman 和 node.js 也没有。所以有时可能会发现后端的同学说, 他们在 postman 或 同源的 swagger 上测试了接口没问题,没发生跨域错误。相信真正了解 CORS 的同学不会以此为依据, 去证明跨域有没有被正确处理,因为那毫无说服力。

思考,为什么解释 CORS 要从 XHR 入手开始,因为从实践来看他们之间有着极大的联系, 不过我暂时没有证据证实 CORS 的作用范围仅限于浏览器之XHR,那么为什么下面通过 script、img 和 iframe 的 src 请求非同源资源不会发生跨域错误, 而通过 axios(本质是 XHR)会发生跨域错误呢?JSONP 不正是利用了 script src 这一特点吗?如果你感兴趣, 可以通过下面两种方式请求非同源资源去验证。

```html

cors demo

script src 请求非同源资源,不会发生跨域错误

```

image.png

四、其他附加的跨域解决参考方案

①vue 开发环境使用 vue-cli 中的 proxy 处理。

vue-cli 3+ 中

js proxy: { '/api': { // 对 '/api' 开头的接口地址进行代理 target: 'http://www.example.org', // target host 目标地址(通常是接口地址) changeOrigin: true, // needed for virtual hosted sites ws: true, // proxy websockets // 代理websockets pathRewrite: { '^/api/old-path': '/api/new-path', // rewrite path // 重写路径 '^/api/remove/path': '/path', // remove base path // 移除基础路径 }, router: { // when request.headers.host == 'dev.localhost:3000', // override target 'http://www.example.org' to 'http://localhost:8000' // 当请求头的host地址为dev.localhost:3000时, // 将目标地址'http://www.example.org'重写为'http://localhost:8000' 'dev.localhost:3000': 'http://localhost:8000', }, }, '/foo': { // 对 '/foo' 开头的接口地址进行代理 target: '<other_url>' } }

vue-cli 2中 js proxyTable: { '/api': { target: 'http://14.11.11.11:8999/', // localhost=>target 目标接口地址 changeOrigin: true, pathRewrite: { '^/api': '/' // 路径重写 } } },

② nginx 代理转发请求到不同源示例

```nginx

tomcat项目 外部访问地址:域名/

location /{
proxy_pass http://location:8088 }

nginx项目 外部访问地址:域名/wx/

location /wx/{ proxy_pass http://location:8081 } ```

③ jsonp 技术(实际项目中选用过)。

大多通过 nginx 代理解决或前后端配合解决

④ window.postMessage(实际项目中未选用过)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值