Chrome S3 Cloudfront: в начальном запросе XHR отсутствует заголовок "Access-Control-Allow-Origin"
У меня есть веб-страница ( https://smartystreets.com/contact), которая использует jQuery для загрузки некоторых файлов SVG из S3 через CloudFront CDN.
В Chrome я открою окно Incognito, а также консоль. Тогда я буду загружать страницу. По мере загрузки страницы я обычно получаю от 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, иначе зачем доставлять контент. Но тогда почему контент не будет доставлен при начальном просмотре страницы?
Я работаю в Google Chrome на MacOS. Firefox без проблем получает файлы каждый раз. Опера НИКОГДА не получает файлы. 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
8 ответов
Вы делаете два запроса на один и тот же объект, один из HTML, другой из XHR. Второй сбой, потому что Chrome использует кэшированный ответ от первого запроса, который не имеет Access-Control-Allow-Origin
заголовок ответа.
Зачем?
Ошибка Chromium 409090 Не удается выполнить перекрестный запрос из кэша после кэширования обычного запроса, и эта проблема "не будет устранена" - они считают, что их поведение правильное. Chrome считает, что кэшированный ответ пригоден для использования, по- видимому, потому что ответ не включает Vary: Origin
заголовок.
Но S3 не возвращает Vary: Origin
когда объект запрашивается без Origin:
заголовок запроса, даже если CORS настроен на ведро. Vary: Origin
отправляется только когда Origin
заголовок присутствует в запросе.
И CloudFront не добавляет Vary: Origin
даже когда Origin
находится в белом списке для пересылки, что по определению должно означать, что изменение заголовка может изменить ответ - вот почему вы пересылаете и кэшируете заголовки запроса.
CloudFront получает пропуск, потому что его ответ был бы правильным, если бы S3 был более правильным, поскольку CloudFront действительно возвращает его, когда он предоставляется S3.
S3, немного пушистый. Это не так, чтобы вернуться Vary: Some-Header
когда не было Some-Header
в запросе.
Например, ответ, который содержит
Vary: accept-encoding, accept-language
указывает, что исходный сервер мог использовать запрос
Accept-Encoding
а такжеAccept-Language
поля (или их отсутствие) как определяющие факторы при выборе контента для этого ответа. (выделение добавлено)
Очевидно, что Vary: Some-Absent-Header
допустимо, поэтому S3 будет правильным, если он добавлен Vary: Origin
на его ответ, если CORS настроен, так как это действительно может изменить ответ.
И, видимо, это заставило бы Chrome делать правильные вещи. Или, если это не будет делать правильно в этом случае, это будет нарушать MUST NOT
, Из того же раздела:
Исходный сервер может отправить
Vary
со списком полей для двух целей:
- Сообщать получателям кэша, что они
MUST NOT
используйте этот ответ для удовлетворения более позднего запроса, если только у более позднего запроса нет тех же значений для перечисленных полей, что и у исходного запроса (Раздел 4.1 [RFC7234]). Другими словами, Vary расширяет ключ кеша, необходимый для сопоставления нового запроса с сохраненной записью кеша....
Итак, S3 действительно SHOULD
возвращаться Vary: Origin
когда CORS настроен на ведро, если Origin
отсутствует в запросе, но это не так.
Тем не менее, S3 не является строго неправильным для того, чтобы не возвращать заголовок, потому что это только SHOULD
не MUST
, Опять же из того же раздела RFC-7231:
Исходный сервер
SHOULD
отправляет поле заголовка Vary, когда его алгоритм выбора представления изменяется в зависимости от аспектов сообщения запроса, отличных от метода и цели запроса,...
С другой стороны, можно утверждать, что Chrome должен неявно знать, что изменение Origin
заголовок должен быть ключом кеша, потому что он может изменить ответ таким же образом Authorization
может изменить ответ.
... если дисперсия не может быть пересечена или исходный сервер не был специально настроен для предотвращения прозрачности кэша. Например, нет необходимости отправлять
Authorization
имя поля вVary
потому что повторное использование между пользователями ограничено определением поля [...]
Точно так же повторное использование в разных источниках возможно ограничено природой Origin
но этот аргумент не является сильным.
tl; dr: Вы, очевидно, не можете успешно извлечь объект из HTML, а затем успешно извлечь его снова с запросом CORS с Chrome и S3 (с или без CloudFront) из-за особенностей реализации.
Временное решение:
Это поведение можно обойти с помощью CloudFront и Lambda@Edge, используя следующий код в качестве триггера Origin Response.
Это добавляет 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, где этот код был опубликован изначально.
Решение Lambda@Edge, приведенное выше, приводит к полностью корректному поведению, но вот две альтернативы, которые вы можете найти полезными, в зависимости от ваших конкретных потребностей:
Альтернатива /Hackaround #1: Подделать заголовки CORS в CloudFront.
CloudFront поддерживает пользовательские заголовки, которые добавляются к каждому запросу. Если вы установите Origin:
при каждом запросе, даже если он не является кросс-источником, это обеспечит правильное поведение в S3. Параметр конфигурации называется Custom Origin Headers, а слово "Origin" означает нечто совершенно иное, чем в CORS. Конфигурирование пользовательского заголовка, подобного этому, в CloudFront перезаписывает то, что отправлено в запросе, с указанным значением, или добавляет его, если оно отсутствует. Если у вас есть только один источник доступа к вашему контенту через XHR, например https://example.com
Вы можете добавить это. С помощью *
сомнительно, но может работать для других сценариев. Тщательно обдумайте последствия.
Альтернатива /Hackaround #2: Используйте "пустой" параметр строки запроса, который отличается для HTML и XHR или отсутствует у того или другого. Эти параметры обычно называются x-*
но не должно быть x-amz-*
,
Допустим, вы составляете имя x-request
, Так <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">
, При доступе к объекту из JS не добавляйте параметр запроса. CloudFront уже делает правильные вещи, кэшируя различные версии объектов, используя Origin
заголовок или его отсутствие как часть ключа кеша, потому что вы перенаправили этот заголовок в поведение кеша. Проблема в том, что ваш браузер этого не знает. Это убеждает браузер в том, что на самом деле это отдельный объект, который необходимо запросить снова, в контексте CORS.
Если вы используете эти альтернативные предложения, используйте одно или другое, а не оба.
По состоянию на ноябрь 2021 года CloudFront напрямую поддерживает политики заголовков ответов. К ним относятся CORS, безопасность и пользовательские заголовки. Больше нет необходимости вставлять собственные заголовки с помощью функций Lambda@Edge или CloudFront.
Возможно, что еще более приятно, больше нет необходимости добавлять Vary в качестве пользовательского заголовка. Новая реализация CORS в политиках заголовков включает дополнительную логику для установки соответствующих заголовков, таких как Vary, в соответствии со стандартом выборки.
Я думаю, что принятый здесь ответ устарел и в любом случае мне не подходит.
Я использовал «CORS-with-preflight-and-SecurityHeadersPolicy», управляемый AWS, и это решило мои проблемы с CORS.
Я не знаю, почему вы получаете такие разные результаты из разных браузеров, но:
X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==
Эта строка прямо здесь - это то, что (если вы можете привлечь их внимание) инженер CloudFront или Служба поддержки будет использовать для выполнения одного из ваших неудавшихся запросов. Если запрос поступает на сервер CloudFront, он должен иметь этот заголовок в ответе. Если этот заголовок отсутствует, то, скорее всего, запрос где-то завершится неудачей, прежде чем он попадет в CloudFront.
Есть еще одно более простое решение, которое работает для меня с использованием атрибута HTML, называемогоcrossorigin='anonymous'
как подробно здесь. По сути, вы можете добавить этот атрибут как таковой:
<img src="your_image_url_here" crossorigin='anonymous'>
и это, по сути, сделает ваш «первый» запрос к изображению запросом CORS, теперь, если вы попытаетесь снова получить то же изображение через XHR, даже если Chrome решит использовать кеш (кэшированный ответ для первого запроса), все будет в порядке как теперь будет в комплектеAccess-Control-Allow-Origin
заголовок.
Принятое решение устраняет проблему, но не является наиболее эффективным, особенно для дистрибутивов CloudFront, которые обслуживают динамический контент. Настройка кэширования заголовков с помощью белого списка приводит к тому, что CloudFront кэширует несколько версий запрошенного объекта в зависимости от заголовка. Это означает, что внутри CloudFront может потребоваться многократное извлечение объекта из источника S3. Передача данных из S3 в CloudFront бесплатна, но это не учитывает дополнительную задержку.
Альтернативным решением здесь было бы отключить конфигурацию CORS в корзине S3 и вместо этого вручную установить заголовки CORS с помощью функции Lambda@Edge, настроенной в ответе средства просмотра. Функция могла выглядеть следующим образом:
'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;
}
У меня не было репутации, чтобы комментировать принятый ответ, но я хотел помочь кому-либо еще, у кого возникли подобные проблемы.
Короче говоря, я считаю, что AWS что-то изменила, так что принятый лямбда-код решения больше не работает (возможно, если / когда вы переключитесь на новую реализацию политики кэширования CloudFront?)
headers['vary']
не является ложным, поэтому обходной путь никогда не запускается.
Это фиксированная лямбда согласно решению dobesv в исходном сообщении на форуме ( https://forums.aws.amazon.com/thread.jspa?messageID=796312):
'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 = [];
for(const hdr of ['Origin', 'Access-Control-Request-Headers', 'Access-Control-Request-Method']) {
if(!headers['vary'].some(h => h.value === hdr)) {
headers.vary.push({key: 'Vary', value: hdr});
}
}
callback(null, response);
};
Чтобы вообще избежать кэширования, вы можете добавить случайный параметр запроса при выполнении запроса через XHR.