我有一个网页(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
。缓存方法相同。转发标头设置为“白名单”,该白名单包括“Access-Control-Request-Headers、Access-Control-Request-Method、Origin”。
事实是,在无缓存浏览器重新加载后,它可以正常工作,这似乎表明 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
答案1
您对同一对象发出了两个请求,一个来自 HTML,一个来自 XHR。第二个请求失败,因为 Chrome 使用了第一个请求的缓存响应,而该请求没有Access-Control-Allow-Origin
响应标头。
为什么?
Chromium 错误 409090 常规请求缓存后,来自缓存的跨源请求失败描述了这个问题,并且这是一个“无法修复”的问题——他们认为他们的行为是正确的。Chrome 认为缓存的响应是可用的,显然因为响应没有包含Vary: Origin
标题。
Vary: Origin
但是,当请求一个没有Origin:
请求标头的对象时,即使在存储桶上配置了 CORS, S3 也不会返回。仅当请求中存在标头Vary: Origin
时才发送。Origin
Vary: Origin
并且,即使将CloudFront 列入转发白名单,它也不会添加Origin
,这在定义上意味着改变标头可能会修改响应——这就是您根据请求标头转发和缓存的原因。
CloudFront 获得通过,因为如果 S3 的响应更正确,它的响应也将是正确的,因为当它由 S3 提供时,CloudFront 确实会返回它。
S3,有点模糊。没有错当请求中Vary: Some-Header
没有时返回。Some-Header
例如,包含以下内容的响应
Vary: accept-encoding, accept-language
表示源服务器可能使用了请求
Accept-Encoding
和Accept-Language
字段(或缺乏)作为选择此回应内容的决定因素。(强调添加)
https://www.rfc-editor.org/rfc/rfc7231#section-7.1.4
显然Vary: Some-Absent-Header
是有效的,因此如果配置了 CORS,S3 将其添加到其响应中将是正确的Vary: Origin
,因为这确实可以改变响应。
显然,这会让 Chrome 做正确的事情。或者,如果它在这种情况下没有做正确的事情,那么它就会违反MUST NOT
。来自同一节:
原始服务器可能会发送
Vary
一个字段列表以达到两个目的:
- 通知缓存接收者,
MUST NOT
除非后续请求与原始请求具有所列字段相同的值,否则他们将使用此响应来满足后续请求([RFC7234] 第 4.1 节)。换句话说,Vary 会扩展将新请求与存储的缓存条目匹配所需的缓存键。
...
因此,当在存储桶上配置了 CORS 时,如果请求中不存在该配置,S3 确实SHOULD
会返回,但事实并非如此。Vary: Origin
Origin
不过,S3 没有返回标头并不完全错误,因为它只是一个SHOULD
,而不是MUST
。同样,来自 RFC-7231 的同一节:
SHOULD
当原始服务器选择表示的算法根据请求消息中除方法和请求目标之外的方面而变化时,原始服务器会发送 Vary 标头字段,...
另一方面,可以说 Chrome 应该隐式地知道改变标Origin
头应该是一个缓存键,因为它可以以同样的方式改变响应Authorization
。
...除非差异无法跨越,或者源服务器被故意配置为阻止缓存透明。例如,无需发送
Authorization
字段名称,Vary
因为跨用户的重用受到字段定义的限制[...]
类似地,跨来源的重用可以说受到性质的限制,Origin
但这种论点并不有力。
总结:由于实现上的特殊性,您显然无法成功地从 HTML 中获取对象,然后使用 Chrome 和 S3(带或不带 CloudFront)以 CORS 请求的形式再次成功地获取它。
解决方法:
可以使用 CloudFront 和 Lambda@Edge 解决此行为,并使用以下代码作为 Origin Response 触发器。
这会添加Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origin
到没有Vary
标头的任何来自 S3 的响应中。否则,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 中启用正确的行为。配置选项称为自定义源标头,其中“源”一词的含义与 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 访问对象时,不要添加查询参数。CloudFront 已经做了正确的事情,通过使用标头Origin
或缺少标头作为缓存键的一部分来缓存对象的不同版本,因为您在缓存行为中转发了该标头。问题是,您的浏览器不知道这一点。这让浏览器相信这实际上是一个需要在 CORS 上下文中再次请求的单独对象。
如果您使用这些替代建议,请使用其中一个,而不是同时使用两者。
答案2
自 2021 年 11 月起,CloudFront 直接支持响应标头策略。这些包括 CORS、安全性和自定义标头。不再需要通过 Lambda@Edge 或 CloudFront 函数注入自定义标头。
也许更令人高兴的是,不再需要将 Vary 添加为自定义标头。Headers Policys 中的新 CORS 实现包括额外的逻辑,可根据获取标准设置适当的标头,例如 Vary。
答案3
我认为这里接受的答案已经过时了,而且无论如何对我来说都不起作用。
我使用了 AWS 管理的“CORS-with-preflight-and-SecurityHeadersPolicy”,这解决了我的 CORS 问题
答案4
公认的解决方案解决了该问题,但它的性能不是最好的,特别是对于提供动态内容的 CloudFront 发行版而言。设置标头缓存使用白名单会导致 CloudFront 根据标头缓存请求对象的多个版本。这意味着 CloudFront 内部可能需要多次从 S3 源重新获取对象。从 S3 到 CloudFront 的数据传输是免费的,但这不考虑额外的延迟。
此处的替代解决方案是禁用 S3 存储桶上的 CORS 配置,而是使用查看器响应上配置的 Lambda@Edge 函数手动设置 CORS 标头。该函数可能如下所示:
'use strict';
const AllowedOriginRegex = /^(.*\.)?example\.com$/;
exports.handler = async (event = {}) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
if (!response.headers.vary) {
response.headers.vary = [
{key: 'Vary', value: 'Origin'},
{key: 'Vary', value: 'Access-Control-Request-Headers'},
{key: 'Vary', value: 'Access-Control-Request-Method'},
];
}
const origin = request.headers.origin && request.headers.origin[0].value;
if (origin && AllowedOriginRegex.test(origin)) {
response.headers['access-control-allow-origin'] = [
{key: 'Access-Control-Allow-Origin', value: origin},
];
response.headers['access-control-allow-methods'] = [
{key: 'Access-Control-Allow-Methods', value: 'GET, HEAD'},
];
response.headers['access-control-max-age'] = [
{key: 'Access-Control-Max-Age', value: '3600'},
];
}
return response;
}