Привет! Меня зовут Ивасюта Алексей, я техлид команды 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.
Полезные ссылки
Предыдущая статья: Ультимативный гайд по HTTP. Структура запроса и ответа