Назад | Перейти на главную страницу

Chrome S3 Cloudfront: нет заголовка Access-Control-Allow-Origin в начальном запросе XHR

У меня есть веб-страница (https://smartystreets.com/contact), который использует jQuery для загрузки некоторых файлов SVG с S3 через CloudFront CDN.

В 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 все в порядке, иначе зачем доставлять контент. Но тогда почему контент не может быть доставлен при первом просмотре страницы?

Я работаю в Google Chrome на macOS. У 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

Вы делаете два запроса для одного и того же объекта, один из 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#section-7.1.4

Ясно, 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.

Это добавляет 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: подделать заголовки CORS в CloudFront.

CloudFront поддерживает настраиваемые заголовки, которые добавляются к каждому запросу. Если вы установите Origin: для каждого запроса, даже если он не является перекрестным, это обеспечит правильное поведение в S3. Параметр конфигурации называется Custom Origin Headers, где слово «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 не добавляйте параметр запроса. CloudFront уже поступает правильно, кэшируя разные версии объектов с помощью Origin заголовок или его отсутствие как часть ключа кеша, потому что вы перенаправили этот заголовок в своем поведении кеша. Проблема в том, что ваш браузер этого не знает. Это убеждает браузер в том, что на самом деле это отдельный объект, который необходимо снова запросить в контексте CORS.

Если вы используете эти альтернативные предложения, используйте одно или другое, а не оба.

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

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

В этой строке находится то, что (если вы можете привлечь их внимание) CloudFront или инженер службы поддержки будут использовать для отслеживания одного из ваших неудавшихся запросов. Если запрос поступает на сервер CloudFront, он должен содержать этот заголовок в ответе. Если этого заголовка нет, значит, запрос, скорее всего, завершился ошибкой до того, как попадет в CloudFront.

Принятое решение устраняет проблему, но не является наиболее эффективным, особенно для дистрибутивов 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;
  const origin = request.headers.origin;

  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;
}