Chrome S3 Cloudfront: у початковому запиті XHR немає заголовка "Access-Control-Allow-Origin"


30

У мене є веб-сторінка ( 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 не має проблем із отриманням файлів кожного разу. Опера НІКОЛИ не отримує файли. 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

Ви маєте рацію - видалення та відтворення є крайніми і просто ніколи не знадобляться. Чи можете ви показати нам запит браузера та заголовки відповідей на невдалий запит? А може бути для успішного запиту саме того самого об’єкта?
Майкл - sqlbot

@ Michael-sqlbot, я сподівався, що ви завітаєте за URL-адресою ( smartystreets.com/contact ) і побачите, чи те саме відбувається на вашій машині. :) Цікаво про помилки в тому, що окрім помилки в консолі, браузер повідомляє про стан 200, посилаючись на те, що він використовує зображення "(з кеш-диска)", що не може бути можливим із Incognito, я думка. Навіть після того, як я очищую локальний кеш.
SunSparc

1
Так, люди настільки часто "складають" доменні імена (які виявляються справжніми сайтами, але не про сайт, про який йдеться), що я спочатку не розумів, що ви дали справжнє, правильне посилання на ваш сайт. Дякую за це, ви можете проігнорувати мій запит. Я можу дублювати проблему. Це здається проблемою, що стосується клієнта. Я переслідую теорію.
Майкл - sqlbot

Я думаю, ви можете помилитися з тим, що це проблема на стороні клієнта. Зображення пов'язані з тегами A у HTML, а потім виглядає, що їх знову запитують у jQuery. Можливо, помилка в одному дзвінку, а 200 - в іншому.
SunSparc

1
Я вважаю саме таким. Chrome і S3 взаємодіють таким чином, що порушує запит CORS, який слід за запитом, що не відповідає CORS, для того ж об'єкта. Можливо, обидва вони помиляються ... але, мабуть, жодне з них не помиляється. Я не думаю, що ви можете це виправити, не зберігаючи дві копії об'єкта за допомогою різних клавіш ... або не використовуючи два різних дистрибутива CloudFront (різні імена хостів), щоб ви не робили як запит CORS, так і не-CORS. Я напишу це з деталями того, як я приходжу до цього висновку, якщо вам подобається.
Майкл - sqlbot

Відповіді:


55

Ви робите два запити на один і той же об’єкт, один з HTML, один з XHR. Другий не працює, оскільки Chrome використовує кешований відповідь з першого запиту, який не має Access-Control-Allow-Originзаголовка відповіді.

Чому?

Chromium bug 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 не є абсолютно помилковим, що не повертає заголовок, оскільки це лише a SHOULD, не a 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.

Якщо ви використовуєте ці альтернативні пропозиції, використовуйте те чи інше - не те і інше.


5
Ваша відповідь - рятівник, чудова відповідь. Ти врятував мені серйозний час.
мтюрт

Привіт, я не використовую Cloudfront для мого s3, тому цей спосіб вирішення проблем не допомагає, чи я можу щось зробити?
Джеффін

1
@Jeffin, альтернатива №2 вище буде працювати лише для S3, без CloudFront. Додавання ?x-some-key=some-valueпараметра довільного рядка запиту переконує браузер у тому, що запит відрізняється.
Майкл - sqlbot

1
@ Michael-sqlbot: Так, працював як шарм
Джеффін

1
@Lionel так, це виглядає правильно.
Майкл - sqlbot

1

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

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

У цьому рядку є те, що (якщо ви зможете привернути їх увагу) інженер CloudFront або підтримки використовуватиме для виконання одного з ваших невдалих запитів. Якщо запит потрапляє на сервер CloudFront, він повинен мати цей заголовок у відповіді. Якщо цього заголовка немає, то, ймовірно, запит десь не вдається, перш ніж він потрапить до CloudFront.


Дякую, я побачу, чи зможу я отримати відповіді на форумах AWS.
SunSparc

1
Можливо, вам доведеться заплатити $ 29 за підтримку розробника. Це банальна сума грошей для будь-якого бізнесу, враховуючи, скільки коштує людина часу.
Тім

1
@ Зверніть увагу, що підтримка розробників - це не просто 29 доларів. Це базова ціна. Якщо 3% від вашої щомісячної рахунку AWS становить> = 29 доларів, ви платите 3% замість основи.
Майкл - sqlbot

Спасибі @ Майкл-sqlbot, я цього не усвідомлював. Я знаю, що ціна підтримки може швидко збільшитися, коли у вас є речі, такі як зарезервовані екземпляри, але я ніколи не дивився на ціни розробника, коли у вас є багато ресурсів.
Тім
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.