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 поля (или их отсутствие) как определяющие факторы при выборе контента для этого ответа. (выделение добавлено)

https://tools.ietf.org/html/rfc7231

Очевидно, что Vary: Some-Absent-Header допустимо, поэтому S3 будет правильным, если он добавлен Vary: Origin на его ответ, если CORS настроен, так как это действительно может изменить ответ.

И, видимо, это заставило бы Chrome делать правильные вещи. Или, если это не будет делать правильно в этом случае, это будет нарушать MUST NOT, Из того же раздела:

Исходный сервер может отправить Vary со списком полей для двух целей:

  1. Сообщать получателям кэша, что они 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.

снимок экрана из AWS CloudFront, показывающий конфигурацию для устранения проблем CORS при первоначальном запросе XHR

Я не знаю, почему вы получаете такие разные результаты из разных браузеров, но:

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.

Другие вопросы по тегам