CORS,全名为跨域资源共享,是为了让不同网站的页面之间互相访问数据的机制。简单来说,CORS 的工作机制是这样的:网站 A 请求网站 B 的资源,网站 A 发起的请求会在 Origin
请求头上带上自己的源(origin
)信息,如果网站 B 返回的响应头里有Access-Control-Allow-Origin
响应头,且响应头的值是网站 A 的源(或者是*
),那么网站 A 就能成功访问到这份资源,否则就报跨域错误。
浏览器在哪些情况下会发起 CORS 请求,哪些情况下发起非 CORS 请求,是有严格规定的。比如在一般的 <img>
标签下发起的就是个非 CORS 请求,而在XHR/fetch
下默认发起的就是 CORS 请求;还比如在一般的<script>
标签下发起的是非 CORS 请求(所以才能有 jsonp),而在新的 <script type="module"
下发起的是 CORS 请求。
CORS 请求会带上 Origin
请求头,用来向别人的网站表明自己是谁;非 CORS 请求不带Origin
头。根据网站有没有根据 Origin
请求头动态返回不同的Access-Control-Allow-Origin
响应头,我把 CORS 请求的响应分成了两种类型:
无条件型 CORS 响应
将Access-Control-Allow-Origin
固定写死为*
(允许任意网站访问)、或者特定的某一个源(只允许这一个网站访问),不论请求头里的 Origin
是什么,甚至没有 Origin
也一样返回那个值。
条件型 CORS 响应
条件型 CORS 响应又分为两种情况:
1. 区分对待有无 Origin
请求头
有Origin
请求头才会返回Access-Control-Allow-Origin
响应头,没有就不返回。
2. 区分对待不同的 Origin
请求头
如果想允许特定的某些个网站访问自己的资源,由于Access-Control-Allow-Origin
被设计为不支持返回多个源,这就需要根据 Origin
请求头的值来动态的判断出要不要加 Access-Control-Allow-Origin
了。
比如我们想只允许 *.taobao.com
下的页面访问,当接受到的请求包含 Origin: https://foo.taobao.com
时,需要返回 Access-Control-Allow-Origin: https://foo.taobao.com
;当接受到的请求包含Origin: https://bar.taobao.com
时,需要返回 Access-Control-Allow-Origin: https://bar.taobao.com
;当接受到的请求包含 Origin: https://foo.tmall.com
时,就不返回Access-Control-Allow-Origin
头了。
条件型 CORS 响应下的缓存错乱问题
简单来说,我们浏览器里的缓存是以 URL 为 key 的,一个 URL 对应一个缓存。但如果浏览器访问了两个 URL 相同但 CORS 响应头不应该相同的资源(即条件型 CORS 响应),会如何呢?
接着上一小节举的例子,比如在同一个浏览器下,先打开了foo.taobao.com
上的一个页面,访问了我们的资源,这个资源被浏览器缓存了下来,和资源内容一起缓存的还有Access-Control-Allow-Origin: https://foo.taobao.com
响应头。这时又打开 bar.taobao.com
上的一个页面,这个页面也要访问那个资源,这时它会读取本地缓存,读到的 Access-Control-Allow-Origin
头是缓存下的 https://foo.taobao.com
而不是自己想要的 https://bar.taobao.com
,这时就报跨域错误了,虽然它应该是能访问到这份资源的。
上面举的例子是“区分对待不同的Origin
请求头”这类条件型 CORS 响应下引起的缓存错乱,这种问题是需要用户访问多个网站(foo.taobao.com
和bar.taobao.com
)后才可能触发的问题。“区分对待有无Origin
请求头”也可能会造成类似的问题,而且在同一个站点下就有可能触发,比如用户先访问了foo.taobao.com
的一个页面 A,页面 A 里用<img>
标签加载了一张图片,注意这时候这张图片已经被浏览器缓存了,并且缓存里没有 Access-Control-Allow-Origin
响应头,因为<img>
发起的请求不带Origin
请求头,此时用户又访问了foo.taobao.com
的另一个页面 B,页面 B 里用 XHR 请求同一张图片,结果读了缓存,没有发现 CORS 响应头,报了跨域错误。在一些场景下,页面 A 和页面 B 有可能会是同一个页面,也就是说在同一个页面里就有可能触发这个问题。
使用 Vary: Origin 让同一个 URL 有多份缓存
有一个 HTTP 响应头叫Vary
,vary 这个单词的意思是“变化”、“不同”的意思,Vary
响应头就是让同一个 URL 根据某个请求头的不同而使用不同的缓存。比如常见的Vary: Accept-Encoding
表示客户端要根据Accept-Encoding
请求头的不同而使用不同的缓存,比如 gizp 的缓存一份,未压缩的缓存为另一份。
在 CORS 的场景下,我们需要使用Vary: Origin
来保证不同网站发起的请求使用各自的缓存。比如从foo.taobao.com
发起的请求缓存下的响应头是:
Access-Control-Allow-Origin: https://foo.taobao.com
Vary: Origin
的话,bar.taobao.com
在发起同 URL 的请求就不会使用这份缓存了,因为Origin
请求头变了。还有<img>
标签发起的非 CORS 请求缓存下的响应头是:
Vary: Origin
的话, 在使用 XHR 发起的 CORS 请求也不会使用那份缓存,因为Origin
请求头从无到有,也算是变了。
Fetch 规范中专门讲了Vary: Origin
在 CORS 响应中该如何使用 fetch.spec.whatwg.org/#cors-proto…,其中第一段是:
If CORS protocol requirements are more complicated than setting `Access-Control-Allow-Origin` to * or a static origin, `Vary` is to be used.
翻译一下就是“如果你的 Access-Control-Allow-Origin
响应头不是简单的写死成了*
或者某一个特定的源(就是我总结的条件型 CORS 响应),那么你就应该加上Vary: Origin
响应头。
In particular, consider what happens if `Vary` is not used and a server is configured to send `Access-Control-Allow-Origin` for a certain resource only in response to a CORS request. When a user agent receives a response to a non-CORS request for that resource (for example, as the result of a navigation request), the response will lack `Access-Control-Allow-Origin` and the user agent will cache that response. Then, if the user agent subsequently encounters a CORS request for the resource, it will use that cached response from the previous non-CORS request, without `Access-Control-Allow-Origin`.
这第二段是特别指出“区分对待有无 Origin
请求头”的同时不加Vary:Origin
头会引起缓存错乱问题。
真实案例
Amazon S3,全名为亚马逊简易存储服务,可以上传任意的资源文件,然后提供 HTTP 协议方式访问。既然是个共用的第三方服务,当然就有配置 CORS 响应头的功能,然而它们就犯了规范中专门强调的这个错误:没有Origin
请求头就不返回Access-Control-Allow-Origin
,同时Vary: Origin
也没有返回。
这个 bug 已经存在至少 7 年了,Chrome 的 bug 列表已经有一堆 WontFix 的 bug:
bugs.chromium.org/p/chromium/…
bugs.chromium.org/p/chromium/…
bugs.chromium.org/p/chromium/…
bugs.chromium.org/p/chromium/…
bugs.chromium.org/p/chromium/…
还有 Stack Overflow 上也很多人问的,你可以在 Google 里搜索 Vary: Origin CORS,前几跳结果里就有。不过不知道为什么,Amazon 就是不修。
和 Amazon S3 对标的服务国内也有很多,比如阿里云的 OSS。是的,阿里云的 OSS 也有同样的 bug,有人也反馈过,还写了 demo 页面,这个页面里先用普通的<img>
对一张图片发起了请求(非 CORS,不带Origin
),然后又用带crossorigin
属性的<img>
对同一张图片发起请求(CORS 请求,带Origin
),结果后者报错了,但如果禁用浏览器缓存,就不会报错:
而且比 S3 错误更大的地方是,S3 最起码在有Origin
请求头的时候是会返回Very: Origin
的,而 OSS 在任何时候都不返回,也就是说前面我举例的因“区分对待不同的Origin
请求头” 引起的缓存错乱问题在 OSS 上也一并存在。和 Amazon 一样,阿里云目前也没修复该 bug。
如何解决
如果服务提供商就是不修,只能自己解决。可以通过增加额外的 URL 参数的方式,比如在非 CORS 请求场景下不加额外参数,在 CORS 场景下加个 ?cors
,这样就不会使用同一份缓存了。
总结
如果你要自己实现 CORS 功能,注意遵守下面的准则:如果是写死的 Access-Control-Allow-Origin
,一定不要加 Vary: Origin
,如果是根据 Origin
请求头动态计算出的Access-Control-Allow-Origin
,一定要始终加上Vary: Origin
,即便在没有 Origin
请求头的情况。
当然,本文讨论的仅限可缓存的静态资源,如果是为动态接口设置 CORS,反正都不允许缓存,当然也就没这个问题了。