转:条件型 CORS 跨域响应下因缺失 Vary: Origin 导致的缓存错乱问题
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也一样返回那个值。
上面举的例子是“区分对待不同的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 有可能会是同一个页面,也就是说在同一个页面里就有可能触发这个问题。
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也没有返回。