Наверняка большинство веб-разработчиков когда-то сталкивались с CORS при выполнении межсайтовых запросов из JavaScript. Причём на эти запросы либо приходили ответы со статусом 403 Forbidden, либо сам браузер отказывался работать с запросом или даже успешным ответом. Предлагаю разобраться с темой CORS.
Что такое CORS
CORS — это протокол межсайтового совместного использования ресурсов (Cross-Origin Resource Sharing), который описывает правила выполнения межсайтовых запросов при помощи JavaScript. Спецификация CORS описана в стандарте Fetch Standard WHATWG.
При помощи CORS владельцы сайтов могут определять правила использования их сайтов в сценариях JavaScript, выполняющихся на сторонних сайтах.
Принцип работы
При выполнении запроса из сценария JavaScript браузер добавляет заголовок Origin
, содержащий адрес сайта, с которого отправляется запрос. Для целевого сайта наличие заголовка Origin
в запросе говорит о том, что запрос был сформирован JavaScript, и к этому запросу нужно применить правила CORS. Если целевой сайт разрешает запросы с указанными параметрами к себе из JavaScript, то результатом выполнения запроса будет успешный ответ, в противном случае — ответ с кодом 403 Forbidden
. Если целевой сайт в принципе не поддерживает CORS, то в его ответах будут отсутствовать заголовки, специфичные для CORS. Браузер в этом случае не разрешит JavaScript дальнейшую обработку ответа, даже если он был успешным, а в консоли разработчика вы можете наблюдать примерно такое сообщение об ошибке:
Запрос из постороннего источника заблокирован: Политика одного источника запрещает чтение удаленного ресурса на http://localhost:8083/api. (Причина: отсутствует заголовок CORS «Access-Control-Allow-Origin»). Код состояния: 200
Действия браузера при отправке запроса из JavaScript различаются в зависимости от того, является ли запрос «простым» или «сложным». В ранних версиях спецификации CORS простыми назывались запросы, не отличающиеся от запросов, которые может выполнить браузер без JavaScript: запросы на получение ресурсов и отправка форм. Запрос считается «сложным», если выполняется хотя бы одно из следующих условий:
- Используется метод, отличный от
GET
,HEAD
иPOST
- Используются нестандартные заголовки, например
X-CUSTOM-HEADER
- Стандартный заголовок содержит нестандартное для веба значение, например
Content-Type: application/json
Простые запросы
Для выполнения простого запроса браузеру не требуется каких-то дополнительных действий — он просто отправляет его целевому сайту. При наличии данного заголовка целевой сайт должен проверить, может ли вызывающий сайт выполнять запросы, и если это так, то должен быть возвращён успешный ответ, содержащий кроме всего прочего заголовки, описывающие настройки CORS сайта:
Access-Control-Allow-Origin
(обязательный заголовок)Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Схематично CORS-запрос можно изобразить следующим образом:
Примеры простых запросов в JavaScript:
1 2 3 4 5 6 7 8 9 10 11 |
// Простой GET-запрос fetch("http://localhost:8081") .then(_ => console.log("Success!")); // Простой POST-запрос let form = new FormData() form.set("foo", "bar") fetch("http://localhost:8081", { method: "POST", body: form }).then(_ => console.log("Success!")); |
Подробно CORS-заголовки запросов и ответов я разберу ниже.
Для простых запросов, которые не содержат тела можно включать режим no-cors
, который отключает поддержку CORS. Но в этом случае браузер будет игнорировать любые параметры запроса, которые делают его сложным, а ответ будет недоступен для JavaScript. Для серверной стороны такие запросы ничем не будут отличаться от «собственных», поскольку в них будет отсутствовать заголовок Origin
.
Пример запроса с режимом no-cors
:
1 2 3 4 5 6 7 |
// Простой GET-запрос в режиме no-cors fetch("http://localhost:8081", {mode: 'no-cors'}) .then(response => { console.log("Success!"); // Будет 0, т.к. данные ответа недоступны JS в режиме no-cors console.log(response.status); }); |
Сложные запросы
Перед выполнением сложного запроса браузер должен выполнить предварительный (preflight) запрос, задача которого — получить от сайта параметры CORS до выполнения основного запроса. Для предварительного запроса используется метод OPTIONS
и заголовки Access-Control-Request-Method
и Access-Control-Request-Headers
. Ответ на предварительный запрос может содержать кроме указанных выше заголовков дополнительные:
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Max-Age
Если ответ на предварительный запрос был успешным, а целевой запрос соответствует параметрам, полученным в ответе на предварительный запрос, то браузер попытается выполнить целевой запрос. В противном случае браузер заблокирует выполнение целевого запроса.
Заголовки
Теперь предлагаю более подробно разобрать заголовки используемые в межсайтовых запросах и ответах.
Origin
Как уже было сказано выше, запросы, отправляемые из сценариев JavaScript, должны содержать заголовок Origin
, содержащий адрес сайта, с которого они выполняются.
Пример заголовка: Origin: http://localhost:8080
Access-Control-Request-Method
При помощи заголовка Access-Control-Request-Method
браузер спрашивает у сайта в предварительном запросе, может ли он выполнить целевой запрос с указанным методом. Значением данного заголовка является строка, при этом допустимые значения не ограничиваются HTTP-методами, могут использоваться и методы из расширений HTTP, например, WebDAV:
Access-Control-Request-Method: PROPFIND
Ответом на данный заголовок является Access-Control-Allow-Methods
.
Access-Control-Request-Headers
При помощи заголовка Access-Control-Request-Headers
браузер спрашивает у сайта в предварительном запросе, может ли он выполнить целевой запрос с указанными заголовками. В качестве значения передаётся список нестандартных заголовков, разделённых запятой, например:
Access-Control-Request-Headers: Authorization, Content-Type
Ответом на данный заголовок является Access-Control-Allow-Headers
.
Access-Control-Allow-Origin
При помощи заголовка Access-Control-Allow-Origin
ответа серверная сторона указывает, скрипты на каких сторонних сайтах могут оправлять запросы. В качестве значения может указываться адрес сайта, указанный в заголовке Origin
запроса, которому разрешено отправлять запросы, либо *
, в этом случае запросы могут отправлять вообще все сайты.
Access-Control-Allow-Credentials
Данный заголовок указывает, разрешает ли целевой сайт передачу в межсайтовых запросах так называемых учётных данных (например, сессионных файлов Cookie или заголовка Authorization
). В качестве значения заголовок может принимать true
или false
.
При этом стоит отметить, что передача учётных данных возможна только при точном указании сайта в заголовке Access-Control-Allow-Origin
, в случае использования *
попытка передать учётные данные приведёт к ошибке.
Access-Control-Expose-Headers
При помощи заголовка Access-Control-Expose-Headers
сайт сообщает, какие заголовки ответа могут быть доступны для скрипта. Заголовок содержит список заголовков, доступных скрипту, разделённых запятой.
Пример: Access-Control-Expose-Headers: X-HDR-1, X-HDR-3
Например, если ответ содержит указанный выше заголовок, а также заголовки X-HDR-1
, X-HDR-2
и X-HDR-3
, то значение заголовка X-HDR-2
для скрипта будет недоступным, т.к. его нет в заголовке Access-Control-Expose-Headers
.
Access-Control-Allow-Methods
При помощи заголовка ответа Access-Control-Allow-Methods
сайт сообщает, какие методы могут использоваться в запросах к нему. Данный заголовок содержит список методов, разделённых запятой.
Пример: Access-Control-Allow-Methods: GET,DELETE
Значение заголовка Access-Control-Allow-Methods
не ограничено стандартными для HTTP методами, вполне могут использоваться методы из расширений HTTP, например WebDAV.
Access-Control-Allow-Headers
При помощи заголовка Access-Control-Allow-Headers
сайт сообщает, какие нестандартные заголовки могут быть использованы в запросах. В качестве значения указывается список заголовков, разделённых при помощи запятой.
Пример: Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age
При помощи заголовка Access-Control-Max-Age
сайт указывает, сколько времени в секундах браузер должен хранить полученные настройки CORS.
Пример: Access-Control-Max-Age: 5
Заголовки Access-Control-Allow-Methods
, Access-Control-Allow-Headers
и Access-Control-Expose-Headers
могут иметь значение *
, т.е. разрешать использование всех методов и заголовков при условии, что заголовок Access-Control-Allow-Credentials
имеет значение false
.
Резюме
CORS специфицирует правила выполнения межсайтовых запросов, выполняемых из сценариев JavaScript, добавляя в запросы и ответы заголовки Origin
и Access-Control-*
. Если настройки CORS целевого сайта не позволяют выполнять запросы из JavaScript запрашивающего сайта, то в ответ он вернёт ответ с кодом состояния 403 Forbidden
. Если целевой сайт в принципе не поддерживает CORS, то браузер откажет JavaScript в дальнейшей обработке ответа, даже если он будет успешным.
Запросы являются простыми и не требуют дополнительных действий от браузера, если не отличаются от запросов, которые могут быть сформированы самим браузером без использования JavaScript. Режим no-cors
отключает поддержку CORS лишь для простых запросов без тела, но в этом случае ответ будет недоступен для JavaScript.
Запросы, использующие нестандартные для веба методы, заголовки или значения заголовков, считаются сложными, и перед их выполнением браузер должен выполнить предварительный запрос, цель которого — получить настройки CORS от целевого сайта.