Куки
Кука — небольшое именованное значение, которое сервер просит браузер сохранить и присылать обратно при каждом следующем обращении к сайту. Это единственный способ, которым HTTP — протокол, ничего не помнящий между запросами, — узнаёт, что два обращения пришли от одного и того же человека.
Что такое кука и зачем она нужна
Проблема. HTTP не хранит состояния. Пользователь ввёл логин и пароль, сервер его узнал — а следующий запрос от того же браузера приходит как от совершенно незнакомого клиента. Всё, чем сервер располагает между двумя запросами, — это то, что он сам попросил браузер запомнить.
Решение. Сервер отправляет куку заголовком Set-Cookie, браузер её сохраняет и
прикладывает заголовком Cookie ко всем последующим запросам к этому сайту. На этом
устроены сессии, «запомнить меня», выбранная тема оформления и язык, корзина до
регистрации, защита от подделки запросов.
Сам обмен прост. Сложность — в атрибутах, которыми кука сопровождается. Кука без
HttpOnly доступна любому скрипту на странице. Кука без SameSite уезжает на чужой сайт
вместе с запросом, который подделал кто-то третий. Кука без Secure идёт по открытому
каналу. Ошибка в любом из них не даёт ни исключения, ни предупреждения — браузер просто
ведёт себя не так, как вы ожидали, и узнаёте вы об этом позже и не оттуда.
Поэтому куки во фреймворке описываются объектом, который отказывается собираться неправильно, а не строкой заголовка, которую надо не забыть составить верно.
Быстрый старт
use Flytachi\Winter\Kernel\Http\Cookie\Cookie;
#[PostMapping('login')]
public function login(#[RequestBody] LoginForm $form): ResponseEntity
{
$token = $this->auth->authenticate($form->email, $form->password);
Cookie::add(Cookie::make('sid', $token)->expiresIn(3600));
return ResponseEntity::noContent();
}
#[GetMapping('me')]
public function me(): ResponseEntity
{
$token = Cookie::get('sid');
if ($token === null) {
return ResponseEntity::unauthorized(['message' => 'Не выполнен вход']);
}
return ResponseEntity::ok($this->auth->userByToken($token));
}
#[PostMapping('logout')]
public function logout(): ResponseEntity
{
Cookie::forget('sid');
return ResponseEntity::noContent();
}Ничего инициализировать не нужно: роутер подготавливает работу с куками в начале каждого запроса, до того как управление дойдёт до контроллера.
Один заголовок, много значений
Set-Cookie — единственный заголовок HTTP, который допустимо повторять: три куки
отправляются тремя заголовками. Поэтому у кук отдельный путь в ответ, а не
->header('Set-Cookie', …): карта заголовков ключуется по имени, и вторая кука затёрла бы
первую.
Справочник
Cookie
Cookie — фасад для работы с куками текущего запроса. Через него читают то, что прислал
браузер, и отправляют то, что должно к нему уйти.
Все методы статические, но состояние у фасада на запрос, а не на процесс: под Swoole данные живут в контексте корутины, обслуживающей запрос, и два одновременных запроса не видят кук друг друга. В режиме, где на процесс приходится один запрос, состояние хранится в статическом поле — там изоляция обеспечивается самим процессом.
Куки, которые вы отправляете, записываются в ответ сразу, ровно как заголовки, а не копятся до конца обработки. Разница проявляется на ошибке:
Cookie::forget('sid'); // сессия погашена — кука уже в ответе
throw new ResponseException('Сессия истекла', HttpCode::UNAUTHORIZED);Если бы куки отправлялись в конце успешного пути, эта — самая нужная — потерялась бы именно тогда, когда запрос завершился ошибкой, и браузер остался бы с мёртвой сессией.
Cookie::get()
Возвращает значение куки, присланной браузером.
Значение приходит уже раскодированным: если при отправке оно было закодировано (а по умолчанию это так), обратное преобразование выполняется здесь.
Синтаксис
public static function get(string $name): ?stringПараметры
$name — имя куки. Регистр важен: именно в таком виде её прислал клиент.
Возвращает
Строку со значением, либо null, если куки с таким именем в запросе не было.
Пример
$theme = Cookie::get('theme') ?? 'light';Cookie::has()
Сообщает, прислал ли браузер куку с указанным именем.
Отличается от get() !== null одним случаем, и он не надуманный: consent= — это
присланная кука с пустым значением. get() вернёт пустую строку, а has() — true,
и для флагов согласия или отметок «показано» разница существенная: пустая строка означает
«ответил», отсутствие — «не ответил».
Синтаксис
public static function has(string $name): boolПараметры
$name — имя куки.
Возвращает
true, если кука была прислана, даже с пустым значением.
Пример
if (!Cookie::has('cookie_consent')) {
return ResponseView::view('banners/cookie-consent');
}Cookie::all()
Возвращает все куки запроса.
Синтаксис
public static function all(): arrayВозвращает
Массив «имя → значение» в том порядке, в каком куки прислал клиент. Пустой массив, если кук не было или запроса нет вовсе — например, в консольной команде.
Пример
foreach (Cookie::all() as $name => $value) {
$this->logger->debug('Кука в запросе', ['name' => $name, 'length' => strlen($value)]);
}Cookie::make()
Создаёт описание куки, готовое к отправке, с учётом текущего запроса и настроек приложения.
Отличается от SetCookie::make() двумя вещами. Во-первых, проставляет
Secure, если запрос пришёл по HTTPS: объект-значение схему запроса не видит, а ошибиться
здесь можно в обе стороны, и обе ошибки тихие — кука с Secure, отправленная по обычному
HTTP, браузером молча выбрасывается. Во-вторых, применяет
умолчания приложения, если они заданы.
Это тот вариант, который нужен прикладному коду.
Синтаксис
public static function make(string $name, string $value = ''): SetCookieПараметры
$name — имя куки. Должно состоять из букв, цифр и части знаков препинания; пробел,
кавычка, точка с запятой и скобки недопустимы. При нарушении будет выброшено
InvalidArgumentException.
$value — значение. По умолчанию пустое. Кодируется при отправке, поэтому подставлять
можно любой текст.
Возвращает
Объект SetCookie с проставленными умолчаниями. Дальше его настраивают
цепочкой вызовов и отправляют через Cookie::add().
Пример
$cookie = Cookie::make('theme', 'dark')
->expiresIn(60 * 60 * 24 * 365)
->httpOnly(false); // читается скриптом на страницеCookie::add()
Отправляет куку клиенту — записывает её в ответ текущего запроса.
Запись происходит немедленно, а не в конце обработки. Поэтому кука, поставленная до выброшенного исключения, всё равно доедет до браузера.
Синтаксис
public static function add(SetCookie $cookie): voidПараметры
$cookie — описание куки, созданное через Cookie::make() или
SetCookie::make().
Ошибки
LogicException — если вызвать вне обработки запроса, например из консольной команды.
Отправлять некуда, а молча проглоченная кука выглядит для вызывающего как «браузер её
проигнорировал», и отладка такого стоит несоизмеримо дороже, чем понятная ошибка.
InvalidArgumentException — если кука собрана некорректно; список случаев — в разделе
«Что не соберётся».
Пример
Cookie::add(Cookie::make('sid', $token)->expiresIn(3600));Cookie::forget()
Просит браузер удалить куку.
Отдельной команды «удалить куку» в HTTP нет: удаление — это отправка куки с пустым значением и сроком жизни в прошлом. Метод делает именно это, избавляя от необходимости собирать такую куку вручную.
Синтаксис
public static function forget(string $name, string $path = '/', ?string $domain = null): voidПараметры
$name — имя удаляемой куки.
$path — путь, с которым кука была поставлена. По умолчанию /.
$domain — домен, с которым кука была поставлена. По умолчанию null, то есть кука
без домена.
Пример
Cookie::forget('sid');
Cookie::forget('admin_pref', '/admin');Путь и домен обязаны совпасть с теми, что были при установке
Для браузера путь и домен — часть личности куки, а не дополнение к ней. Кука,
поставленная на /admin, не удаляется удалением на /: браузер видит запрос на удаление
другой куки, а исходная продолжает жить и присылаться.
Это самая частая причина жалоб «выход из системы не работает».
Cookie::defaults()
Задаёт атрибуты, с которыми будет создаваться каждая кука из
Cookie::make().
Настраивается один раз при старте приложения. Принимает не готовый образец, а функцию,
и разница существенная: функция выполняется после подстановки Secure по схеме
запроса, поэтому приложение может перекрыть и её — например, когда TLS терминируется на
прокси, а до приложения запрос доходит уже по обычному HTTP.
Синтаксис
public static function defaults(?Closure $configure): voidПараметры
$configure — функция, принимающая SetCookie и возвращающая SetCookie. Значение
null сбрасывает ранее заданные умолчания.
Пример
use Flytachi\Winter\Kernel\Http\Cookie\{Cookie, SameSite, SetCookie};
protected static function configure(ApplicationArguments $args): void
{
Cookie::defaults(fn(SetCookie $cookie) => $cookie
->domain('example.com')
->sameSite(SameSite::Strict));
}Примечание
На SetCookie::make() умолчания не действуют — та версия остаётся
чистой и ничего не знает ни о запросе, ни о настройках.
Cookie::init() и Cookie::clear()
Служебные методы жизненного цикла. Прикладной код их не вызывает.
init() вызывается роутером в начале каждого запроса: разбирает присланные куки и
запоминает ответ, в который будут писаться отправляемые. clear() сбрасывает это
состояние — нужен между запросами в режиме «один запрос на процесс» и в тестах.
public static function init(HttpRequest $request, HttpResponse $response): void
public static function clear(): voidSetCookie
SetCookie — описание одной куки: имя, значение и все атрибуты, с которыми она уйдёт к
браузеру.
Объект неизменяемый: каждый метод настройки возвращает новый экземпляр, а исходный остаётся прежним. Благодаря этому заготовку можно держать в одном месте и раздавать, не опасаясь, что её испортят из другого:
$base = SetCookie::make('sid')->secure()->httpOnly()->expiresIn(3600);
$forFirstUser = $base->value($firstToken); // $base не изменился
$forSecondUser = $base->value($secondToken);Умолчания подобраны безопасными:
| Атрибут | Значение | Почему так |
|---|---|---|
Path |
/ |
кука видна всему сайту |
HttpOnly |
включён | скрипты на странице её не прочитают |
SameSite |
Lax |
то же, что подставляют современные браузеры |
| Срок жизни | сессия | кука умирает вместе с окном браузера |
Secure |
выключен | см. ниже |
Secure в умолчания не входит намеренно. Объект-значение не видит схему запроса, а кука
с Secure, отправленная по обычному HTTP, браузером молча выбрасывается. Схему
подставляет Cookie::make() — у него есть живой запрос.
SetCookie::make()
Создаёт описание куки без оглядки на запрос и настройки приложения.
Нужен там, где запроса нет: в тестах, в фоновой задаче, при сборке заготовки на старте.
В контроллере обычно используют Cookie::make().
Синтаксис
public static function make(string $name, string $value = ''): SetCookieПараметры
$name — имя куки.
$value — значение, по умолчанию пустое.
Ошибки
InvalidArgumentException — если имя пустое или содержит символ, недопустимый в имени
куки: пробел, =, ;, ,, кавычку, скобку, слэш, управляющий символ.
Пример
use Flytachi\Winter\Kernel\Http\Cookie\SetCookie;
$cookie = SetCookie::make('locale', 'ru');SetCookie::forget()
Собирает куку, удаляющую другую куку с тем же именем.
То же, что делает Cookie::forget(), но возвращает объект вместо того,
чтобы сразу отправить. Пригодится, когда удаление нужно приложить к конкретному ответу, а
не к текущему запросу.
Синтаксис
public static function forget(string $name, string $path = '/', ?string $domain = null): SetCookieПараметры
Совпадают с Cookie::forget(): имя, а также путь и домен, с которыми
кука была поставлена.
Возвращает
Куку с пустым значением и сроком в прошлом.
Результат
sid=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/; HttpOnly; SameSite=LaxПример
return ResponseEntity::noContent()->cookie(SetCookie::forget('sid'));expiresIn()
Задаёт срок жизни куки в секундах от текущего момента.
Самая частая форма: «час», «месяц», «год» естественно выражаются длительностью, а не датой.
Синтаксис
public function expiresIn(int $seconds): SetCookieПараметры
$seconds — сколько кука должна прожить. Ноль или отрицательное значение означает
удаление — на этом и построен SetCookie::forget().
Возвращает
Новый объект с заданным сроком.
Пример
Cookie::make('sid', $token)->expiresIn(3600); // час
Cookie::make('remember', $token)->expiresIn(60 * 60 * 24 * 30); // месяцРезультат
sid=abc; Expires=Tue, 19 Aug 2025 11:40:00 GMT; Max-Age=3600; Path=/; HttpOnly; SameSite=LaxОтправляются оба атрибута сразу, и это не избыточность: Max-Age понимают современные
браузеры, Expires — самые старые, а при наличии обоих стандарт предписывает
руководствоваться Max-Age. Пара безопасна, а не противоречива.
expiresAt()
Задаёт срок жизни куки конкретным моментом.
Нужен там, где важна дата, а не длительность: «до конца акции», «до полуночи», «до истечения подписки».
Синтаксис
public function expiresAt(DateTimeInterface|int $moment): SetCookieПараметры
$moment — объект даты либо unix-метка времени.
Возвращает
Новый объект с заданным сроком.
Пример
Cookie::make('promo', 'summer-sale')
->expiresAt(new DateTimeImmutable('2026-09-01 00:00:00'));Примечание
Момент в прошлом означает удаление куки. Отрицательный Max-Age при этом не отправляется —
вместо него уходит Max-Age=0, поскольку отрицательное значение часть клиентов считает
ошибкой разбора.
session()
Убирает срок жизни: кука живёт до закрытия браузера.
Это поведение по умолчанию, поэтому метод нужен в одном случае — когда срок был задан раньше по цепочке и его надо отменить.
Синтаксис
public function session(): SetCookieПример
$base = SetCookie::make('csrf')->expiresIn(3600);
// Для этой формы токен не должен переживать окно браузера.
$oneTab = $base->value($token)->session();path()
Задаёт префикс URL, для которого браузер будет присылать куку.
Синтаксис
public function path(string $path): SetCookieПараметры
$path — путь, по умолчанию /. Кука с путём /admin присылается для /admin и
/admin/users, но не для /.
Возвращает
Новый объект с заданным путём.
Пример
Cookie::make('admin_sidebar', 'collapsed')->path('/admin');Примечание
Сужение пути — не средство защиты: любая страница сайта всё равно может отправить запрос
на /admin и кука уйдёт вместе с ним. Это способ не таскать лишние данные в каждом
запросе.
domain()
Задаёт домен, для которого действует кука.
Синтаксис
public function domain(?string $domain): SetCookieПараметры
$domain — домен. Значение null (умолчание) означает «только текущий хост, без
поддоменов» — и это безопаснее: кука не уедет на соседний поддомен, которым может владеть
другая команда. Указанный домен покрывает и его поддомены: example.com включает
api.example.com.
Возвращает
Новый объект с заданным доменом.
Пример
// Кука должна работать и на example.com, и на api.example.com
Cookie::make('sid', $token)->domain('example.com');Примечание
Ведущая точка (.example.com) — наследие старого стандарта. Современные браузеры её
игнорируют, писать её не нужно.
httpOnly()
Запрещает или разрешает доступ к куке из JavaScript.
Включён по умолчанию. С ним кука не видна в document.cookie, а значит найденная на
странице уязвимость с внедрением скрипта не сможет её выкрасть. Для токена сессии это
обязательное условие.
Синтаксис
public function httpOnly(bool $httpOnly = true): SetCookieПараметры
$httpOnly — false открывает куку скриптам.
Возвращает
Новый объект.
Пример
Cookie::make('sid', $token)->httpOnly(); // включён и так, но пусть будет явно
Cookie::make('theme', 'dark')->httpOnly(false); // читает скрипт оформленияПримечание
Выключайте только для значения, которое действительно читает JavaScript самой страницы: тема, свёрнутая панель, номер шага мастера. Токен сессии таким значением не бывает.
secure()
Требует, чтобы кука передавалась только по HTTPS.
Синтаксис
public function secure(bool $secure = true): SetCookieПараметры
$secure — false снимает требование.
Возвращает
Новый объект.
Пример
SetCookie::make('sid', $token)->secure();Примечание
При создании через Cookie::make() этот атрибут уже проставлен, если
запрос пришёл по HTTPS. Вызывать вручную нужно в двух случаях: когда куку собирают через
SetCookie::make(), и когда TLS терминируется на прокси, а до
приложения запрос доходит по обычному HTTP.
sameSite()
Определяет, будет ли браузер прикладывать куку к запросам, инициированным другим сайтом.
Это основная защита от подделки межсайтовых запросов. Без неё браузер отправит вашу куку вместе с запросом, который сформировала чужая страница, и сервер не отличит его от настоящего действия пользователя.
Синтаксис
public function sameSite(?SameSite $sameSite): SetCookieПараметры
$sameSite — вариант перечисления SameSite, либо null, чтобы не
отправлять атрибут вовсе и оставить решение браузеру.
Возвращает
Новый объект.
Значения
| Значение | Когда браузер приложит куку | Для чего подходит |
|---|---|---|
SameSite::Lax |
свои запросы плюс переходы по ссылке | сессия обычного сайта; значение по умолчанию |
SameSite::Strict |
только свои запросы | банк, админка. Переход по ссылке извне выглядит как «не вошёл» |
SameSite::None |
всегда, включая чужие сайты | виджет, встроенный в чужую страницу. Требует Secure |
null |
атрибут не отправляется | решает браузер; сегодня это то же, что Lax |
Пример
use Flytachi\Winter\Kernel\Http\Cookie\SameSite;
Cookie::make('sid', $token)->sameSite(SameSite::Strict);partitioned()
Помещает куку в отдельное хранилище для каждого сайта, в который встроена страница.
Механизм называется CHIPS и нужен виджетам. Если ваш виджет встроен и в a.example, и в
b.example, без этого атрибута он получает одну общую куку и может по ней связать
пользователя между сайтами. С ним у каждого встраивающего сайта своя.
Синтаксис
public function partitioned(bool $partitioned = true): SetCookieПараметры
$partitioned — false снимает атрибут.
Возвращает
Новый объект.
Пример
Cookie::make('widget_state', $state)
->secure()
->sameSite(SameSite::None)
->partitioned();Примечание
Требует Secure. Кука с Partitioned, но без него, собрана не будет —
см. отказы.
raw()
Отправляет значение как есть, без кодирования.
По умолчанию значение кодируется: a b/c уезжает как a%20b%2Fc и возвращается обратно
раскодированным. Отключать это стоит для значений, которые и так состоят из безопасных
символов, — JWT, шестнадцатеричная подпись, идентификатор, — чтобы промежуточное звено не
закодировало их второй раз.
Синтаксис
public function raw(bool $raw = true): SetCookieПараметры
$raw — false возвращает кодирование.
Возвращает
Новый объект.
Ошибки
Сырое значение проверяется при отправке: пробел, кавычка, запятая, точка с запятой и
управляющие символы приведут к InvalidArgumentException. Без этой проверки точка с
запятой оборвала бы куку, а всё, что за ней, браузер прочитал бы как атрибуты.
Пример
Cookie::make('token', $jwt)->raw();value()
Заменяет значение, сохраняя все атрибуты.
Метод существует ради заготовок: настроить один раз набор атрибутов и дальше выдавать по нему куки с разными значениями.
Синтаксис
public function value(string $value): SetCookieПараметры
$value — новое значение.
Возвращает
Новый объект с тем же набором атрибутов.
Пример
$sessionCookie = SetCookie::make('sid')
->secure()
->httpOnly()
->sameSite(SameSite::Strict)
->expiresIn(3600);
Cookie::add($sessionCookie->value($token));Чтение атрибутов
Одиннадцать методов, возвращающих то, что в объекте уже задано. Прикладному коду они обычно не нужны — используются в тестах и там, где куку перед отправкой проверяют.
public function getName(): string
public function getValue(): string
public function getPath(): string
public function getDomain(): ?string
public function getSameSite(): ?SameSite
public function getExpires(): ?int // unix-метка, если срок задан моментом
public function getMaxAge(): ?int // секунды, если срок задан длительностью
public function isSecure(): bool
public function isHttpOnly(): bool
public function isPartitioned(): bool
public function isRaw(): boolПример
$cookie = Cookie::make('sid', $token)->expiresIn(3600);
if (!$cookie->isSecure() && $environment === 'production') {
throw new RuntimeException('В продакшене сессионная кука обязана быть Secure');
}toHeader()
Собирает значение заголовка Set-Cookie — то, что уйдёт в ответ.
Вызывать вручную не нужно: это делает ответ при отправке. Метод полезен в тестах, где проверяют получившиеся байты, и при отладке.
Синтаксис
public function toHeader(?int $now = null): stringПараметры
$now — точка отсчёта для перевода длительности в дату и обратно. По умолчанию текущее
время; в тестах передают фиксированное, чтобы результат не зависел от момента запуска.
Возвращает
Строку заголовка без его имени.
Ошибки
InvalidArgumentException — если кука собрана некорректно. Проверка выполняется именно
здесь, а не в сеттерах: атрибуты задаются по одному, и порядок ->sameSite(None)->secure()
так же законен, как обратный.
Пример
$header = SetCookie::make('sid', 'abc')
->expiresIn(3600)
->secure()
->toHeader(now: 1755600000);Результат
sid=abc; Expires=Tue, 19 Aug 2025 11:40:00 GMT; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=LaxSameSite
Перечисление значений атрибута SameSite. Три варианта, разобранных
выше.
enum SameSite: string
{
case Lax = 'Lax';
case Strict = 'Strict';
case None = 'None';
}Что не соберётся
Объект отказывается отдавать куку, которую браузер всё равно молча выбросил бы. Проверка происходит при сборке заголовка, то есть при отправке.
| Ситуация | Что произойдёт |
|---|---|
SameSite::None без Secure |
InvalidArgumentException — браузер такую куку отвергает |
Partitioned без Secure |
InvalidArgumentException |
raw() со значением, содержащим пробел, ;, , или кавычку |
InvalidArgumentException |
Имя с пробелом, =, ;, ,, скобками, слэшем |
InvalidArgumentException при создании |
| Пустое имя | InvalidArgumentException при создании |
Cookie 'sid': SameSite=None requires Secure, or the browser discards the cookie.Смысл проверок в том, что все перечисленные ошибки тихие: без них кука просто не появилась бы у клиента, а разбираться пришлось бы в браузере, а не в коде.
Чтение через объект запроса
Куки доступны и напрямую у запроса, если он уже инжектирован в метод контроллера. Пара методов названа по образцу заголовков: кука читается так же, как заголовок.
public function getCookie(string $name): ?string
public function getCookies(): arrayПример
#[GetMapping('me')]
public function me(HttpRequest $request): ResponseEntity
{
$token = $request->getCookie('sid');
return ResponseEntity::ok(['authenticated' => $token !== null]);
}Почему возвращается карта строк, а не объекты
В библиотеках других языков этот метод иногда отдаёт массив объектов-кук. Здесь этого намеренно нет: во входящем запросе браузер присылает только пары «имя=значение» — ни срока жизни, ни пути, ни домена в нём нет.
Объекты с пустыми полями создавали бы впечатление, что атрибуты присланной куки можно
прочитать. Карта строк не обещает того, чего в запросе не бывает. Атрибуты — это
SetCookie, то есть исходящая сторона.
Как это устроено внутри
Раздел для любопытных: знать это, чтобы пользоваться куками, не требуется.
Чтение идёт из сырого заголовка, а не из встроенного разбора PHP. У PHP есть готовый массив кук, но он переименовывает имена. Один и тот же запрос:
Cookie: my.sid=1; my sid=2; ok=3
встроенный разбор PHP → ["my_sid", "ok"] точка заменена, вторая кука выброшена
Winter → ["my.sid", "my sid", "ok"]Swoole разбирает заголовок сам и так не поступает — то есть на встроенном разборе два режима работы фреймворка давали бы разные наборы имён, и приложение, работающее в одном, ломалось бы в другом. Поэтому заголовок разбирается собственным кодом одинаково в обоих.
Всё остальное поведение намеренно повторяет привычное: из двух кук с одним именем
побеждает первая, пустое значение сохраняется, имя без = читается как пустая строка.
Запись собирает заголовок сама. Средства выпуска кук, встроенные в PHP и в Swoole,
пишут атрибуты по-разному — разным регистром и с разным кодированием пробела. Строку
собирает toHeader(), и оба режима отправляют одни и те же байты, а тесты,
проверяющие эти байты, что-то значат.
Дальше
- Ответы — метод
cookie()на любом типе ответа - Запросы — что ещё приходит вместе с куками
- Middleware — удобное место, чтобы открыть или погасить сессию
- Локализация — язык, выбранный пользователем, хранится в куке