Cross-Origin Resource Sharing (CORS)

Наверняка большинство веб-разработчиков когда-то сталкивались с 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: запросы на получение ресурсов и отправка форм. Запрос считается «сложным», если выполняется хотя бы одно из следующих условий:

  • Используется метод, отличный от GETHEAD и POST
  • Используются нестандартные заголовки, например X-CUSTOM-HEADER
  • Стандартный заголовок содержит нестандартное для веба значение, например Content-Type: application/json

Простые запросы

Для выполнения простого запроса браузеру не требуется каких-то дополнительных действий — он просто отправляет его целевому сайту. При наличии данного заголовка целевой сайт должен проверить, может ли вызывающий сайт выполнять запросы, и если это так, то должен быть возвращён успешный ответ, содержащий кроме всего прочего заголовки, описывающие настройки CORS сайта:

  • Access-Control-Allow-Origin (обязательный заголовок)
  • Access-Control-Allow-Credentials
  • Access-Control-Expose-Headers

Схематично CORS-запрос можно изобразить следующим образом:

Примеры простых запросов в JavaScript:

Подробно CORS-заголовки запросов и ответов я разберу ниже.

Для простых запросов, которые не содержат тела можно включать режим no-cors, который отключает поддержку CORS. Но в этом случае браузер будет игнорировать любые параметры запроса, которые делают его сложным, а ответ будет недоступен для JavaScript. Для серверной стороны такие запросы ничем не будут отличаться от «собственных», поскольку в них будет отсутствовать заголовок Origin.

Пример запроса с режимом no-cors:

Сложные запросы

Перед выполнением сложного запроса браузер должен выполнить предварительный (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-1X-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-MethodsAccess-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 от целевого сайта.

Полезные ссылки