Chrome S3 Cloudfront:初始XHR请求中没有“ Access-Control-Allow-Origin”标头


30

我有一个网页(https://smartystreets.com/contact),该网页使用jQuery通过CloudFront CDN从S3加载一些SVG文件。

在Chrome浏览器中,我将打开隐身窗口和控制台。然后,我将加载页面。当页面加载时,我通常会在控制台中收到6到8条类似于以下内容的消息:

XMLHttpRequest cannot load 
https://d79i1fxsrar4t.cloudfront.net/assets/img/feature-icons/documentation.08e71af6.svg.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'https://smartystreets.com' is therefore not allowed access.

如果我对页面进行标准重新加载,即使是多次加载,也会继续遇到相同的错误。如果执行Command+Shift+R此操作,则将加载大多数(有时是全部)图像而不会出现XMLHttpRequest错误。

有时即使在图像加载后,我也会刷新,并且其中一幅或多幅图像将无法加载,并XMLHttpRequest再次返回该错误。

我已经检查,更改并重新检查了S3和Cloudfront上的设置。在S3中,我的CORS配置如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedOrigin>http://*</AllowedOrigin>
    <AllowedOrigin>https://*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

(注意:最初只有<AllowedOrigin>*</AllowedOrigin>,有同样的问题。)

在CloudFront中,分发行为设置为允许HTTP方法:GET, HEAD, OPTIONS。缓存的方法是相同的。转发标题设置为“白名单”,该白名单包括“访问控制请求头,访问控制请求方法,来源”。

它在无缓存浏览器重新加载后可以工作的事实似乎表明,S3 / CloudFront方面一切正常,否则为什么要交付内容。但是,为什么不能在初始页面浏览中传递内容?

我正在macOS上使用Google Chrome浏览器。Firefox每次都获取文件没有问题。Opera从不获取文件。Safari会在几次刷新后拾取图像。

使用curl我没有任何问题:

curl -I -H 'Origin: smartystreets.com' https://d79i1fxsrar4t.cloudfront.net/assets/img/phone-icon-outline.dc7e4079.svg

HTTP/1.1 200 OK
Content-Type: image/svg+xml
Content-Length: 508
Connection: keep-alive
Date: Tue, 20 Jun 2017 17:35:57 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Max-Age: 3000
Last-Modified: Thu, 15 Jun 2017 16:02:19 GMT
ETag: "dc7e4079f937e83291f2174853adb564"
Cache-Control: max-age=31536000
Expires: Wed, 01 Jan 2020 23:59:59 GMT
Accept-Ranges: bytes
Server: AmazonS3
Vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
Age: 4373
X-Cache: Hit from cloudfront
Via: 1.1 09fc52f58485a5da8e63d1ea27596895.cloudfront.net (CloudFront)
X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g==

有人建议我删除CloudFront发行版并重新创建。似乎是相当苛刻和不便的修复。

是什么导致此问题?

更新:

从无法加载的图像添加响应头。

age:1709
cache-control:max-age=31536000
content-encoding:gzip
content-type:image/svg+xml
date:Tue, 20 Jun 2017 17:27:17 GMT
expires:2020-01-01T23:59:59.999Z
last-modified:Tue, 11 Apr 2017 18:17:41 GMT
server:AmazonS3
status:200
vary:Accept-Encoding
via:1.1 022c901b294fedd7074704d46fce9819.cloudfront.net (CloudFront)
x-amz-cf-id:i0PfeopzJdwhPAKoHpbCTUj1JOMXv4TaBgo7wrQ3TW9Kq_4Bx0k_pQ==
x-cache:Hit from cloudfront

您是对的-删除和重新创建是极端的,绝对不应该这样做。您能否向我们显示浏览器针对失败请求的请求和响应标头?也许是成功请求完全相同的对象?
Michael-sqlbot

@ Michael-sqlbot,我有点希望您访问URL(smartystreets.com/contact),看看您的计算机上是否正在发生相同的事情。:)关于错误的有趣的事情是,除了控制台中的错误外,浏览器还报告状态为200,并指出它正在使用映像“(来自磁盘缓存)”,这在Incognito中是不可能的,我思想。即使在清除本地缓存之后。
SunSparc

1
是的,人们常常“组成”域名(实际上是真实站点,而不是所讨论的站点),以至于我最初没有意识到您已经为站点提供了正确的链接。谢谢你,你可以不理我的要求。我可以重复这个问题。这似乎是一个客户端问题。我正在追求一种理论。
Michael-sqlbot

我认为您认为这是一个客户端问题可能是正确的。这些图像与HTML中的A标签链接在一起,然后看起来像它们在jQuery中再次被请求。也许错误是来自一个呼叫,而200来自另一个。
SunSparc

1
我相信情况就是如此。Chrome和S3的交互方式中断了针对同一对象的非CORS请求之后的CORS请求。可以说,他们两个都是错误的...但是,可以说,他们都不是错误的。我认为如果不使用不同的密钥存储对象的两个副本,或者使用两个不同的CloudFront发行版(不同的主机名),就无法解决此问题,这样就不会同时发出CORS和非CORS请求。如果您愿意,我将详细说明如何得出这个结论。
Michael-sqlbot

Answers:


55

您正在针对同一对象发出两个请求,一个来自HTML,一个来自XHR。第二个失败,因为Chrome使用第一个请求的缓存响应,该请求没有Access-Control-Allow-Origin响应头。

为什么?

Chromium bug 409090来自缓存的跨域请求在缓存常规请求后失败,描述了此问题,这是“无法解决”的问题-他们认为其行为是正确的。Chrome认为缓存的响应是可用的,这显然是因为该响应未包含Vary: Origin标头。

但是,即使Vary: Origin没有在Origin:请求标头上请求对象,即使在存储桶上配置了CORS,S3也不会返回。 Vary: OriginOrigin在请求中包含标头时发送。

而且,Vary: Origin即使将CloudFront Origin列入转发白名单,CloudFront也不会添加,这从定义上来说应该意味着更改标头可能会修改响应-这就是您针对请求标头转发和缓存的原因。

CloudFront获得通过,因为如果S3更正确,它的响应将是正确的,因为CloudFront在S3提供时会返回此消息。

S3,有点模糊。当请求中没有请求时返回是没有错的。Vary: Some-HeaderSome-Header

例如,包含以下内容的响应

Vary: accept-encoding, accept-language

指示源服务器可能已经使用请求的 Accept-EncodingAccept-Language字段(或其缺乏)作为决定因素,而选择此响应的内容。(添加了重点)

https://tools.ietf.org/html/rfc7231#section-7.1.4

显然,它Vary: Some-Absent-Header是有效的,因此,Vary: Origin如果配置了CORS ,则S3如果在响应中添加了S3,那将是正确的,因为这确实可能会改变响应。

而且,显然,这将使Chrome能够正确执行操作。或者,如果在这种情况下没有做正确的事情,那将违反MUST NOT。在同一部分中:

原始服务器可能发送Vary带有字段列表的两个目的:

  1. 通知缓存接收者他们MUST NOT使用此响应来满足以后的请求,除非后面的请求在列出的字段中具有与原始请求相同的值([RFC7234]的4.1节)。换句话说,Vary扩展了将新请求与存储的缓存项匹配所需的缓存键。

...

因此,如果在存储桶中配置了CORS ,则S3确实SHOULD会返回Vary: Origin,如果Origin请求中不存在,则不是。

不过,S3绝对不错误,因为它不返回标头,因为它只是a SHOULD,不是a MUST。同样,在RFC-7231的同一部分中:

当源服务器SHOULD选择其表示的算法根据请求消息的方法(而不是方法和请求目标)的其他方面而变化时,原始服务器将发送Vary标头字段。

另一方面,可以说Chrome应该隐式地知道更改Origin标头应该是一个缓存键,因为它可以以相同的方式Authorization更改响应,从而可以更改响应。

...除非无法跨越差异或故意将原始服务器配置为防止高速缓存透明。例如,不需要发送Authorization字段名称,Vary因为跨用户的重用受到字段定义的限制[...]

同样,跨源重用可能会受到的性质的限制,Origin但这一论点并不是一个强有力的论点。


tl; dr:由于实现的特殊性,您显然无法从HTML成功获取对象,然后再通过Chrome和S3(带有或不带有CloudFront)作为CORS请求成功地再次获取对象。


解决方法:

使用以下代码作为Origin Response触发器,可以使用CloudFront和Lambda @ Edge解决此问题。

这将增加Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origin来自S3且没有Vary标题的任何响应。否则,Vary响应中的标头将不会被修改。

'use strict';

// If the response lacks a Vary: header, fix it in a CloudFront Origin Response trigger.

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    if (!headers['vary'])
    {
        headers['vary'] = [
            { key: 'Vary', value: 'Access-Control-Request-Headers' },
            { key: 'Vary', value: 'Access-Control-Request-Method' },
            { key: 'Vary', value: 'Origin' },
        ];
    }
    callback(null, response);
};

署名:我还是最初共享此代码的AWS Support论坛上原始帖子的作者。


上面的Lambda @ Edge解决方案可产生完全正确的行为,但是根据您的特定需求,您可能会发现以下两种替代方法很有用:

替代方案/解决方法#1:在CloudFront中伪造CORS标头。

CloudFront支持添加到每个请求的自定义标头。如果Origin:在每个请求上都设置,即使不是跨域请求,也将在S3中启用正确的行为。该配置选项称为“自定义来源标题”,其中“ Origin”一词的含义与CORS中的含义完全不同。在CloudFront中配置这样的自定义标头会使用指定的值覆盖请求中发送的内容,如果不存在,则将其添加。如果您只有一个来源可以通过XHR访问内容,例如https://example.com,您可以添加它。使用*是可疑的,但可能适用于其他情况。仔细考虑其含义。

替代方法/解决方法#2:使用“虚拟”查询字符串参数,该参数对于HTML和XHR而言有所不同,或者彼此都不存在。这些参数通常是命名的,x-*但不应命名 为x-amz-*

假设您组成了这个名字x-request。这样<img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">。从JS访问对象时,请勿添加查询参数。通过使用Origin标头或不包含标头作为缓存键的一部分来缓存对象的不同版本,CloudFront已经在做正确的事情,因为您已在缓存行为中转发了该标头。问题是,您的浏览器不知道这一点。这使浏览器确信,它实际上是一个单独的对象,需要在CORS上下文中再次请求。

如果您使用这些替代建议,请使用一个或另一个-不能同时使用。


5
您的回应是一个救命稻草,一个很好的答案。你为我节省了一些时间。
mtyurt

嗨,我不为我的s3使用cloudfront,所以此解决方法无济于事,我还能做点什么?
Jeffin '18

1
@Jeffin,上面的替代方法#2仅适用于S3,无需CloudFront。添加任意?x-some-key=some-value查询字符串参数将使浏览器确信请求是不同的。
Michael-sqlbot

1
@ Michael-sqlbot:是的,就像一个魅力一样工作
Jeffin '18

1
@Lionel是的,这看起来很正确。
Michael-sqlbot

1

我不知道为什么您会从各种浏览器中得到如此不同的结果,但是:

X-Amz-Cf-Id:wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

在那条直线上,CloudFront或支持工程师将使用(如果您引起他们的注意)跟踪您失败的请求之一。如果请求到达CloudFront服务器,则响应中应具有此标头。如果该标头不存在,则该请求很可能在到达CloudFront之前在某处失败。


谢谢,我将看看是否可以在AWS论坛上得到任何答复。
SunSparc

1
您可能需要支付29美元的开发人员支持费用。考虑到一个人的时间成本,这对于任何企业来说都是微不足道的钱。
蒂姆(Tim)

1
@Tim,请注意,开发人员支持不只是29美元。那是底价。如果您每月AWS账单的3%> = $ 29,您将支付3%而不是基本费用。
Michael-sqlbot

感谢@ Michael-sqlbot,我没有意识到。我知道,当您拥有诸如预留实例之类的东西时,支持价格会迅速上涨,但是当您拥有大量资源时,我从未考虑过开发人员的定价。
蒂姆(Tim)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.