缘起
平时我们总会遇到跨域问题,而跨域请求基本都会看到 OPTIONS,于是为了学(pian)习(zan),在下决定去偷摸的跟踪 OPTIONS 请求,看看它和跨域到底有啥不可告人的交易
从头开始
既然说了跟踪,那当然要从家里开始了,嘘...
RFC
请求
- OPTIONS 方法用于请求关于目标资源可用的通信选项的信息
- 这个方法允许客户端确定指定资源的通信选项或要求
响应
- 服务器响应成功应该在 header 中返回对于当前请求资源服务端所支持的功能
MDN
上面的RFC确实难读,不太像人能看懂的。所以在下斗胆使用人话翻译一下
OPTIONS 方法为给定的 URL 或服务器请求允许的通信选项
上面的话是机翻的MDN,毕竟咱这英语课净睡觉的主看英文还是有点费劲。但是好像还是挺难懂的,那就不能怪我展示了
OPTIONS 请求是用来针对某个资源(URL),向服务端请求这个资源在访问时的一些限制信息(比如 header、method等),然后客户端根据服务端返回的限制信息来决定接下来怎么做
对于浏览器来说,如果 OPTIONS 请求返回的限制信息和我们的真实请求不匹配,接下来就是给我们一个大大的错误信息
窥视金主(跨域)
跨域我相信大家都懂了,不懂的同学自行报课外班补习一下
到这里同学应该都已经了解跨域了,那么基于它们(跨域和OPTIONS)搞交易,当然了,这种事情放到台面不好搞,所以一般都会有一些暗号,幸好,我这种学识渊博的人轻易就给破解了
跨域头
都说了暗号了,当然两边的肯定是不一样的了,又不是复读机
请求
Access-Control-Request-Method:接下来的真实请求将要使用的方法
Access-Control-Request-Headers:真实请求所设置的自定义 header
响应
Access-Control-Allow-Origin
- 真实响应可以被使用的域 (接头人的交好的组织)
- * 表示所有域都可以使用
- 带凭证的请求返回的 * 没有特殊语义,就表示域为 * 的域 (哪有这种域啊喂)
Access-Control-Allow-Methods
- 真实请求可以使用的请求方法 (接头人告诉从哪些路可以到,别迷路了)
- 对于不带凭证的请求,* 表示通配符 (条条大路通资源)
- 对于带凭证的请求,* 没有任何语义,就表示 * 这个请求方法 (哪有这种方法啊喂)
Access-Control-Allow-Headers
- 真实请求可以自定义的头部 (接头人告诉带什么货)
- 对于不带凭证的请求,* 被看作通配符
- 对于带凭证的请求,* 没有任何语义,就表示 * 这个请求方法 (哪有这种请求头啊喂)
Access-Control-Max-Age
- OPTIONS 请求返回的响应可以被缓存的时间,以秒为单位 (交接人告诉过一段时间再来,总来也容易引起注意呀)
Access-Control-Allow-Credentials
- 告知响应是否能被请求代码使用 (交接人发错货了)
- 用于预检请求时,表示真实请求能否带凭证请求
我:到了这里,我看大概也清楚了,它俩(跨域和OPTIONS)确实存在一些交易,而且还是金钱上的交易
某同学:可不是吗,那 OPTIONS 不就是给跨域打工的吗,不就是个马仔吗
表扬一下这个同学啊,大概差不多是这个意思,但是怎么能叫人马仔呢,要叫好员工
出发(请求)
暗号 OPTIONS 已经了解了,剩下就差时间了,排好日程就可以出发。当然我们跨域这么大个金主,肯定不能亏待员工,朝九晚五五天班肯定要满足嘛,不然马儿累死了怎么办
所以为了员工的身心健康,我们有如下策略:对于请求分为简单请求和复杂请求,简单请求我们就不 care 了,复杂请求我们才派我们的 OPTIONS 请求去接头
简单请求
满足以下条件的请求可以看成简单请求
- 仅限于 GET、HEAD、POST 方法
- 能够手动设置的请求头仅限于 Accept、Accept-Language、Content-Language、Content-Type
- Content-Type 仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
- 没有监听 XMLHTTPRequest 对象 upload 属性的事件
- 没有使用 ReadableStream 对象(Fetch)
接下来我们挨个看下,这些限制条件是什么意思
当然,为了验证我们需要写两个简单的 server 来模拟跨域情景,咱代码功底属实太差就不贴代码了,各位同学可以自己去写一下,不需要几行代码
注意,前方群图出没,建议 wifi 下观看,流量请谨慎,到时候和小女生聊天没流量了可别怪我
请求方法
可以看到 HEAD 请求确实没有发送 OPTIONS 预检请求,但是却遇到了跨域错误,这是怎么回事,让我们看下报错信息
可以看到是我们服务端没有返回 Access-Control-Allow-Origin 这个请求头,这说明了虽然简单请求不会发起 OPTIONS 预检请求,但是还是会触发跨域 (毕竟员工不上班公司业务不能停呀)
好了,我们先改动下重启服务端代码,再看下其他请求
可以看到确实 HEAD、GET、POST 方法没有触发预检请求,那接下来看下 PUT 方法呢
嗯,PUT 方法确实触发了预检请求,接下来的方法我们就不一一试验了
到了这里大家可能会有一个疑问,真实请求是等待 OPTIONS 请求返回还是同时发送,我们接下来看一下,稍微改一下服务端代码,在 OPTIONS 请求时等待 30s 后才返回,我们看下结果
同时 pending 了,好像还不好判断,我们改下服务端代码加打印看看
这下懂了,只有 OPTIONS 请求返回了才会发起真实请求。诶,那新问题又来了,如果 OPTIONS 请求返回的限制信息与真实请求的不匹配,还会发起真实请求吗,我们再调整代码来看看
可以看到 OPTIONS 请求返回的限制信息和真实请求不匹配时,浏览器没有选择发起真实请求,而是直接一个错误扔到了我们脸上
当然,我使用的浏览器是 Chrome,其它浏览器的行为可能不一样,比如 OPTISON 请求和真实请求同时发,这个我们就不一一去看了,写文章好累的说
请求头
口干了,喝口水,贴图大家自己看下
文件上传
文件上传时如果有监听 upload 的事件,确实会触发预检请求,没有监听也确实不会触发预检请求
ReadableStream
如果请求中使用了 ReadableStream 也没出现预检请求,代码如下,希望了解的同学能指点一二
复杂请求
显然,不属于简单请求的请求就是复杂请求,对于复杂请求,浏览器就需要派出 OPTIONS 请求发起一个预检请求
比如,我们经常使用的 json 请求,由于设置的 Content-Type 不在简单请求限定的范围内,它就是一个复杂请求,会触发预检请求
带上信物(凭证)
前面我们讲到过带上凭证和不带凭证表现会有些许的不一致,那么到底不一致是什么呢,我们去看下
简单请求
可以看到简单请求在带上凭证之后失败了,报错信息说响应头里缺少 Access-Controll-Allow-Credentials 这个头部信息,说明了虽然请求成功了,但是由于限制信息不匹配(谁让你不和人家搞好关系,带的信物人家都不认),响应还是被浏览器给吃掉了
复杂请求
复杂请求和之前的现象一致,OPTIONS 返回的限制信息和真实请求不匹配就不会发起真实请求
诶,然而就当准备收笔的时候,突然又看到另外一个问题,复杂请求会发起两次请求,一次 OPTIONS 请求,一次真实请求,OPTIONS 请求决定是否发起真实请求,那如果真实请求的响应里没有 Access-Controll-Allow-Credentials 头部会怎么样呢
可以看到如果真实请求没有返回正确的头部信息,我们的请求就失败了。可以看到,带凭证跨域请求的响应 Access-Controll-Allow-Credentials 头部和普通跨域响应的 Access-Controll-Allow-Origin 请求头一样,缺失了就会导致请求失败
既然问题已经弄清楚了,那我们让对方认我们所携带的信物就可以顺利对接上了
我们在服务端加上如下代码
res.setHeader('Access-Control-Allow-Credentials', true);
接下来见证奇迹
请求成功了,cookie 也被正确的带上了
行动结束(结语)
至此,行动结束,没想到人家俩就是简单的雇佣关系,搞得我们费这么大劲去调(wei)查(sui)
当然,也不能说没有什么收获吧,毕竟时间都拿来浪(xue)费(xi)了,咱也该去干点正事了