Привет! Меня зовут Ивасюта Алексей, я техлид команды Bricks в Авито в кластере Architecture, а это мой цикл статей о протоколе HTTP. В первой части мы уже познакомились с версией протокола 1.0 и поговорили о структуре ответов и запросов. Теперь пришло время узнать, что такое Cookies и для чего нужен CORS.

Cookies

HTTP — это протокол без сохранения состояния. Это значит, что когда на сервер поступают два запроса, он не понимает: их послал один клиент или разные. В реальных приложениях постоянно надо идентифицировать пользователя: проверять, авторизован ли он, выводить пользовательские рекомендации, хранить корзину выбранных товаров в магазине. Для этого есть механизм Cookies.

Cookies — это фрагмент данных, в которых сервер передаёт важную информацию о клиенте. Браузер получает этот фрагмент, сохраняет его и с каждым последующим запросом отсылает обратно на сервер. Так он понимает, от какого клиента пришёл запрос.

Куки выглядят, как пара ключ=значение. Для их установки сервер должен в ответе на запрос клиента прислать заголовок Set-Cookie со значением.

Set-Cookie: token=123abc

Как только браузер получит его в ответ на запрос, он сохранит значение куки в Cookie Storage. В каждый последующий запрос браузер будет добавлять сохранённые значения в заголовке Cookie.

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

Обычно кукам устанавливается фиксированное время жизни. Это можно сделать двумя способами: 

  • установить дату истечения срока годности — Expires

  • установить количество секунд, по истечении которых кука перестанет быть актуальной — MAX_AGE.

Допустим, срок жизни куки из предыдущего примера должен истечь 1 января 2030 года. Тогда нужно прислать заголовок со значением куки и датой истечения срока годности, разделёнными точкой с запятой.

Set-Cookie: token=123abc; Expires=Tue, 01 Jan 2030 00:00:00 -0000

Теперь с наступлением 1 января 2030 года кука станет невалидной и удалится.

В альтернативном варианте срок жизни куки должен истечь через две минуты. Тогда вместо даты нужно прислать количество секунд в параметре MAX_AGE:

Set-Cookie: token=123abc; MAX_AGE=120

Разные браузеры могут использовать разные параметры для установки времени жизни куки. Поэтому зачастую сервер присылает оба варианта, а браузер сам выбирает поддерживаемый параметр.

Set-Cookie: token=123abc; Expires=Thu, 05 Aug 2021 18:45:00 -0000; MAX_AGE=120

Стандарт RFC6265 1, описывает куки. Он закрепляет правило, что имена параметров — регистронезависимые. Поэтому эта запись тоже будет валидной:

Set-Cookie: token=123abc; expires=Thu, 05 Aug 2021 18:45:00 -0000; max_age=120

Область видимости Cookies

Параметры domain и path управляют областью видимости куки и определяют, на каких поддоменах и для каких URL они будут работать. Если не указать параметр domain, то он по умолчанию будет равен хосту сайта, например, example.com. Тогда куки не будут работать для поддоменов, например, test.example.com. Если указать значение domain, то куки будет работать для хоста и всех поддоменов.

Параметр path указывает, по каким URL при запросе ресурсов необходимо передавать куку в заголовке. Если установить path=/, то куки будут отправляться для всех запросов. Если указать значение path=/api, то они будут передаваться для всех запросов на ресурсы, URL которых начинается с /api

Расширим предыдущий пример:

Set-Cookie: token=123abc; expires=Thu, 05 Aug 2021 18:45:00 -0000; max_age=120; domain=example.com; path=/api

Уникальный идентификатор куки — их ключ (в примере — token), значения domain и path. Если нужно переопределить значение ранее установленных куки, то эти три параметра должны совпадать. В противном случае браузер создаст новые. А ещё не существует метода для удаления куки. Можно только установить её повторно и указать дату истечения срока годности expires в прошлом или нулевой или отрицательный max_age.

Защита Cookies

Куки представляют два параметра, которые защищают их от различных атак.

Параметр secure говорит о том, что такие куки будут отсылаться только по HTTPS-соединению. Если сайт работает по HTTP, то значение не будет передано. Secure не обеспечивает дополнительного шифрования, поэтому не стоит хранить чувствительные данные в куках.

Параметр httpOnly создаёт куки, к которым нельзя обратиться из кода JavaScript в браузере. Так можно избежать XSS-атак.2

Отправка на сервер

Куки отправляются на сервер при каждом запросе, если не подошла их дата истечения срока годности и параметры path и domain соответствует URL ресурса. Они передаются одной строкой в значении заголовка запроса Cookie и разделяются точкой с запятой.

Cookie: key1=value1; key2=value2;

Директива Евросоюза о Cookie

25 мая 2011 года в силу вступила Директива 2009/136/EC Евросоюза о куки.3 Она требует запрашивать разрешение пользователя на получение и использование информации с его компьютера. Поэтому почти каждый сайт при первом посещении или после очистки кеша выводит баннер с запросом на использование Cookies.

CORS

По умолчанию в браузере действует политика безопасности Same Origin Policy. Это означает, что доступ к ресурсам можно получить, только если источник этих ресурсов и источник запроса совпадают. Это сделано для того, чтобы некий сайт хакеров evil.com не мог получить доступ, например, к почтовому ящику на домене gmail.com и прочитать письма. Эта стратегия работала долгое время, когда JavaScript считался браузерным языком для украшения страничек и не умел делать AJAX-запросы.

Со временем разработчики поняли, что хотят уметь отправлять данные на сервер и получать их при помощи асинхронных запросов. Но политика Same Origin не позволяет сделать этого. После долгих обсуждений был предложен механизм CORS — Cross-Origin Resource Sharing.

Этот механизм использует дополнительные HTTP-заголовки. С ним браузер пользователя получает разрешения на доступ к выбранным ресурсам, если домен сайта и домен запрашиваемого ресурса отличаются.

Например, сайт находится на домене example.com. Запрос отправляется на сайт, который находится на домене web-server.com. Для начала браузер должен понять, сложный этот запрос или простой. Простой запрос отправляется методом GET, POST или HEAD и содержит только следующие заголовки:

  • Accept;

  • Accept-Language;

  • Content-Language;

  • Content-Type со значением application/x-www-form-urlencoded, multipart/form-data или text/plain.

Остальные запросы считаются сложными. Например, если он отправляется методом DELETE или содержит заголовок Authorization. Простые запросы браузер отправляет напрямую на сервер и автоматически добавляет к ним заголовок Origin. Его значение равно URL хоста, с которого отправляется запрос. В нашем примере это Origin: https://example.com

POST /path HTTP/1.0
Host: web-server.com
Origin: example.com
Content-Type: text/plain

Сервер принимает такой запрос и определяет, разрешить хосту получить этот ресурс или отказать в запросе. Для этого в ответе используется специальный заголовок Access-Control-Allow-Origin. Он определяет, с каких источников разрешено принимать запросы. Если указать Access-Control-Allow-Origin: http://example.com, то запрос на ресурс будет всегда разрешён только домену example.com. Если нужно разрешить получать ресурс с любого домена, то в ответе на запрос должен быть заголовок  Access-Control-Allow-Origin: *

Браузер заблокирует ответ на запрос, если:

  • сервер вернёт в ответе заголовок Access-Control-Allow-Origin: null

  • значение не будет совпадать с переданным в заголовке запроса Origin,

  • не будет заголовка. 

Если браузер обнаружит сложный запрос, то сначала отправит серверу preflight — предварительный запроc при помощи HTTP-метода OPTIONS. В запрос при этом добавляется заголовок Access-Control-Request-Method, в значении которого указывается HTTP-метод оригинального запроса.

Если в оригинальном запросе передаются заголовки не из списка разрешённых, то в запрос OPTIONS дополнительно добавляется заголовок Access-Control-Request-Headers. В его значении через запятую перечисляются все заголовки оригинального запроса. 

В случае отправки preflight-запроса сервер также должен вернуть заголовок Access-Control-Allow-Origin с необходимым значением.

Если в запрос добавлен заголовок Access-Control-Request-Method, то сервер в ответе должен указать, какие HTTP-методы разрешено использовать для запроса ресурса в заголовке ответа Access-Control-Allow-Methods.

Если в запрос добавлен заголовок Access-Control-Request-Headers, то сервер в ответе должен указать, какие HTTP-заголовки разрешено использовать для запроса ресурса в заголовке ответа Access-Control-Allow-Headers

Пример ответа на preflight-запрос:

HTTP/1.0 200 OK
Date: Thu, 29 Jul 2021 19:20:01 GMT
Connection: close
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: DELETE, GET, POST
Access-Control-Allow-Headers: X-Custom-Header, Authorization

Браузер получает ответ и после этого сверяет значения Origin, разрешенных заголовков и методов. Если всё совпадает, отправляет оригинальный запрос на сервер. Если разрешённые параметры не совпадают с фактическими, браузер блокирует отправку оригинального запроса и сообщает об ошибке.

Иногда необходимо уметь передавать Cookies при обращении к другим доменам. Заголовок Cookie не относится к списку разрешенных и попадает под политику безопасности. Чтобы их можно было передавать, сервер в ответ на preflight и на оригинальный запрос должен возвращать заголовок Access-Control-Allow-Credentials: true. Для работы куки также нужно возвращать в заголовке Access-Control-Allow-Origin конкретное значение разрешённого хоста.

Ответ от кроссдоменного запроса получен. Допустим, нужно получить доступ к заголовкам в коде JavaScript в браузере и прочитать их значения. Сделать это не так просто, потому что по умолчанию доступ из Java Script для кроссдоменных запросов есть только к следующим заголовкам:

  • Cache-Control

  • Content-Language

  • Content-Length

  • Content-Type

  • Expires

  • Last-Modified

  • Pragma

Если JS попытается прочитать значение другого заголовка, то получит null. Но сервер в ответе на запрос может перечислить заголовки, к которым можно получить доступ из Javascript в заголовке Access-Control-Expose-Headers. Заголовки указываются через запятую. Например, Access-Control-Expose-Headers: Authorization, X-Version.

Заголовок ответа сервера Access-Control-Max-Age сообщает браузеру, насколько предзапрос может быть кэширован и опущен при запросах к серверу. Значение указывается в секундах. Если после первого выполнения preflight-запроса время жизни Max-Age не вышло, то повторной отправки не будет. Оригинальный запрос выполнится сразу.

Про Cookies и CORS на этом — всё. В третьей статье цикла я расскажу о нововведениях в версии HTTP/1.1 и чем она отличается от HTTP/2.

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

  1. Стандарт RFC6265

  2. Что такое XSS атаки

  3. Директива 2009/136/EC

Предыдущая статья: Ультимативный гайд по HTTP. Структура запроса и ответа

Комментарии (2)