跳至主要內容

什么是CORS

PPLong大约 10 分钟学习WebHTTP

SOP与CORS

问题溯源

为什么我会遇到这个问题呢?如果你也有类似的经历,那么接下来的内容就或许能够帮助你解决问题

1. 通过服务器网页上的js脚本想在线获取leetcode的数据

通过网页上的js脚本给leetcode发送请求,这个bug发现花了很长时间,因为在自己的Chrome上以前也发生过类似的CORS请求失败问题,当时没有深究,查了下 下载了Allow CORS这个插件,立马解决了问题。后来浏览器就一直保持Allow CORS打开的情况。
后来自己写hexo-leetcode-calendaropen in new window的js再次通过CORS去访问其他网站,但发现在手机上显示不出来画面和数据(但我自己的客户端上却没有问题)。(当时认为是Canvas在高清的手机屏上绘制存在冲突)又因为不会移动端Chrome调试,查了很久才定位到是网络请求出了问题,这才溯源突然发现CORS的问题,然后一切的一切的一切瞬间是那么豁然开朗,从最开始的网页CORS报错到无法访问Bilibili和YouTube的一些资源,再到现在的脚本绘制不显示,顿时就通了。脚本的主要问题就是 没抓$.get()的报错,导致啥错发生了在浏览器上都不知道

2. 无法访问bilibili和Youtube的部分资源

如果你的Chrome突然无法搜索bilibili,又或者能够正常访问Youtube网页但无法正常观看Youtube视频,那你可能要检查一下你的Chrome插件了,之前一段时间内突然不能访问Bilibili了,不能登录也不能观看视频,Youtube也离奇报错,能够翻墙、正常搜索视频却不能观看。一直以为是代理哪儿设置错了(经典代理背锅)

image-20220426173912299
image-20220426173912299
image-20220426174053301
image-20220426174053301

写完这篇文章后我打开Allow-CORS 抓了下包,发现很多都是CORS error,可能原因是虽然你客户端是不启用CORS了,但我可能有一些脚本不会被Allow-CORS拦截而发送请求。

介绍

Cross-Origin Resource Sharing:一种允许当前域的资源被其他域的脚本请求访问的机制,通常由于同域安全策略(Same Origin Policy),浏览器会禁止这种跨域请求的返回。(注意,只是过滤掉返回的请求,但实际上请求是成功发送给服务端的, 只是发回来的数据被浏览器扔掉了)

Domain 域(协议schema 主机host 端口号port)都必须一致的

什么又是同源策略?举个例子,比如在我的网站上有个js脚本,js脚本要通过get去获取另一个网站的数据并返回,最终在我的网站上显示,这就产生了跨域的请求。本来只是当前浏览器 <---> 我的网站 的双向响应,但由于js脚本发出的请求,就当前浏览器就要去往其他的服务器发送请求(注意,这里并不是网站的服务器去发送请求,而是访问的客户端去发送),这往往可能是不安全的,因为我可以根据Cookie去其他的网站上发送请求

实验

工具:PyCharm 、Tonardo 、Wireshark、Chrome

首先通过fetch去测试在当前域下去访问该域的一些资源,肯定是可行的。

搭建简单的服务器

我先通过基于python的tornado搭建了简易的服务器,监听9002端口并返回数据

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello World")


def makeApp():
    return tornado.web.Application([
        (r"/", MainHandler)
    ])


if __name__ == '__main__':
    app = makeApp()
    app.listen(9002)
    tornado.ioloop.IOLoop.current().start()

CORS访问

我在网站的console上,调用fetch去获取不同域的数据,这里便出现了CORS的限制。“服务器没有返回可以进行跨域访问的说明”

image-20220426155024359
image-20220426155024359

所以这时候问题出在服务器,服务器对发出的resp需要添加CORS的说明响应头

self.set_header("Access-Control-Allow-Origin", "https://pplong.top")

此时浏览器便可以成功打印

小坑

这里还有个跟代理有关的坑点

最开始是用设置了Clash代理的Chrome去做fetch请求,但只返回了Promise对象而未返回数据,后来用未开启代理的Firefox去请求,则得到了正常的数据。未验证但估计跟proxy有关系,可能是数据返回给Proxy延时太久?

Access-Control-Allow-Origin为服务器指明了从那些域来的跨域请求可以我可以接受

Preflight

如果正如之前请求Leetcode API一样,如果加上一个Content Type ,那么一样会报错,这里就涉及到Preflight的知识了

image-20220426162442588
image-20220426162442588
probably-mistake
probably-mistake

Preflight 一般指的就是OPTIONS请求,在浏览器认为即将要执行的请求可能会对服务器造成不可预知的影响时,会由浏览器自动发出,与服务器沟通是否能发送类似的请求。

如何触发

刚刚的GET请求就没有触发Preflight,那具体什么时候会触发浏览器发送该请求呢?

  1. 请求方法:POST GET HEAD
  2. 请求头: AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth
  3. Content-Type:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  4. ......

不满足以上四种条件的都会触发浏览器的预检,所以我这里设置了Content-Type,则触发了预检,来看一下具体抓包的细节。

抓包

这里首先在浏览器内看到第一个包,类型是OPTIONS,把它想发的请求类型、想要运作的请求头都发了出去,并且说明了FetchMode是cors的,随后浏览器便返回一个 405的包,说明请求不被允许

image-20220426163732009
image-20220426163732009

通过服务器中options方法来进行对OPTIONS请求的处理,在tornado中,未经过预检的命令则不会通过OPTIONS,不会进入到对应的请求方法体中,所以要处理这个问题,应该要处理options函数。

回到前面的两个错误响应,首先第一张是因为我只为get请求设置了Allow-Origin,但这是一个预检的OPTIONS请求,而对应options请求的服务器响应没有设置,所以还需要为options设置跨域请求的域名白名单。

第二个错误响应是因为我们设置了可能造成影响的请求头(触发Preflight的根源),服务器没有允许设置了这个类型的请求头可以通过,所以还需要添加Access-Control-Allow-Headers,为这样的请求头放行

    def get(self):
        print("receive!")
        self.set_header("Access-Control-Allow-Origin", "http://www.pplong.top")
        self.write(f"Hello World")

    def options(self):
        self.set_header("Access-Control-Allow-Origin", "http://www.pplong.top")
        self.set_header("Access-Control-Allow-Headers", "Content-Type")
        print("trigger options")

客户端成功响应

Max-Age

Access-Control-Max-Age 指明服务器在指定时间内不用为同一请求执行多次预检,没有在服务器中给客户端返回此值,则多次跨域请求 每次都会触发预检。这个值仅仅是告诉客户端,让他不用做预检处理,而服务端本身是没有什么操作的

一个诡计

由于max-age是只存在浏览器内的,所以这就可能导致一个问题,考虑如下场景:

如果我的服务器最开始是允许某个网站跨域访问并且保持一定时间的max-age内不用预检,但运行一段时间后我反悔了,我在服务器端删去了这个请求头,那请问客户端在max-age内进行跨域访问还会触发预检吗?

答案是否定的! 因为浏览器不会触发预检,所以也就无法进入处理到options方法,那就有可能让访问者对服务器通过危险的请求例如DELETE或PUT等来对数据进行修改,这就造成了隐患!

这里我也进行了实验,先打开服务器端的max-age,让浏览器请求数据并保存max-age,然后服务器删去max-age请求头,让客户端再次请求,可以发现,客户端未发送OPTIONS包而直接发送了POST包。

image-20220426172021100
image-20220426172021100

(即使改变的服务器的数据,一样能成功请求并返回200的响应码)

如果后端人员突然变更该请求头,那么就可能导致安全问题,虽然具体到应用层面,后端设置可能更为复杂,可能有自己的判断标志,但常规来讲这样做可能导致风险。(给自己提个醒🤕, 防止日后出大问题 )

深入

CORS会造成哪些问题?

信息泄露

比如你刚在网站上登录了账户,浏览器保存了cookie,如果这时候你访问另一个恶意网站,那么如果没有同源策略则你的浏览器内的账户cookie可能就会被获取

如何绕过CORS?

JSONP

从script标签的可跨域性入手,且默认是javascript类型执行的

由于script src可跨域获取,所以通过在src中往js请求塞入参数,这个参数是之前script定义的方法 + 参数值来进行,服务端通过取这些参数和值,直接包装成一个函数返回。也就是说我根本可以没有js函数,js请求只是个幌子,因为html5中默认的script类型是text/javascript,所以只要返回来的是与html script关联的正确的函数代码,那就可以执行

let express = require('express');
let app = express();
app.listen(3200, () => {
  console.log('OK');
})
app.get('/getData', (req, res) => {
  // 注意Function.prototype很重要,指定是函数类型(原理还没搞清楚)
  let { callback = Function.prototype } = req.query;
  console.log(Function.prototype)
  const data = {
    name: '张三',
    message: 'Person',
    sex: '男'
  }
  res.send(`${callback}(${JSON.stringify(data)})`);
  // value == jsonpCallback({"name":"张三","message":"Person","sex":"男"})
  console.log('发送成功');
})
<script>
    <!-- HTML内自定义的函数体 -->
	function jsonpCallback(res) {
        console.log("123123")
		console.log(res);
		}
</script>
<script src="http://127.0.0.1:3200/getData?callback=jsonpCallback"></script>
image-20220426200501038
image-20220426200501038

成功打印,但JSONP有缺陷,通过url的方式,只能实现get请求,其他请求就不能执行了,所以范围还是比较局限。因此也就有了CORS

CORS

本文所介绍到的

回顾

回顾前面的问题,尝试先清空缓存后开启Allow-CORS,然后进行未被允许的CORS请求,发现响应码200并返回正常数据,随后进行抓包发现,只发送了一个GET请求,即浏览器自身直接忽略了预检这一步骤。

Bilibili CORS Error中,基本上都是js脚本触发的CORS Error,说明可能js脚本中一些xhr类函数的执行没有被Allow-CORS捕获并重新设置,进入到错误js中调试后发现引发了我们都已熟悉了的这个问题

image-20220426175608360
image-20220426175608360

总结

好了,一句话,罪魁祸首:Allow-CORS全锅! 😆

其实还是一个前后端通信一个比较经典的问题,相信实战的时候肯定还会遇到此类问题,到时候应该还会有新的心得