Перейти к содержанию

Сессии и подписанные cookie

Namespace Keel\Session. Сессия живёт в атрибуте запроса, а не в сервисе, поэтому в worker-режиме ничего не протекает между запросами. Подключение: SessionMiddleware в HttpConfig и SessionConfig в config/session.php.

// config/session.php
return static fn(Env $env): SessionConfig => new SessionConfig(
    secret: $env->require('APP_SECRET'),   // не короче 32 символов
    storage: 'cookie',                     // или 'file'
    ttl: 14 * 86400,                 // секунды бездействия, не меньше 1
);

// контроллер: сессия приходит по типу параметра
#[Post('/login')]
public function login(#[FromBody] Login $cmd, Session $session): array
{
    $session->regenerate();                // новая id, защита от фиксации
    $session->set('user_id', $id);
    return ['ok' => true];
}
Хранилище Где данные Когда подходит
cookie (по умолчанию) в самой cookie, подписанной HMAC-SHA256, со сроком годности нет серверного состояния, масштабируется горизонтально; данные видны клиенту (не секреты), лимит 4 КБ
file <filePath>/sess_<id>.json, в cookie только подписанная id данные скрыты от клиента, больше 4 КБ; нужна общая файловая система или одна нода

Keel\Session\RedisStorage хранит сессии в Redis: RedisConfig(use: ['sessions']) с RedisProvider, см. Кэш. Любое другое хранилище реализует SessionStorageInterface (read, write, destroy) и регистрируется под этим интерфейсом. Реализуйте ещё и SessionExpiry (expiresAt) — тогда middleware сможет продлевать стареющие сессии (см. ниже).

Cookie-хранилище подписано, но не отзываемо. destroy() снимает cookie в браузере, но её копия действительна до истечения срока, а клиент может прислать любую старую cookie, которую сохранил, — состояние в ней можно откатить (использованный flash, счётчик попыток). Авторитетное состояние и сессии, которые нужно уметь завершить с сервера, держите в file или своём хранилище. При cookie-хранилище Session::id() — новое случайное значение на каждый запрос, а regenerate() ничего не меняет, поэтому не привязывайте к id CSRF-токены и блокировки.

Файловое хранилище пишет файлы 0600 в каталог 0700 и отказывается работать с каталогом, который является символической ссылкой. Укажите filePath на свой каталог (var/sessions): значение по умолчанию, <системный temp>/keel-sessions, доступно всем локальным пользователям машины. Параллельные запросы одной сессии не блокируют друг друга: побеждает последняя запись.

API Session: get, set, has, remove, pull (flash), all, clear, regenerate, destroy, id, isNew. Cookie ставится, если сессия менялась, а также когда до истечения осталось меньше половины ttl, так что ttl — настоящий таймаут бездействия: активный пользователь, который ничего не пишет в сессию, не разлогинится. Уничтоженная или опустевшая сессия снимает cookie (Max-Age=0) и удаляет запись в хранилище, включая запись до regenerate() в том же запросе.

Флаги cookie: HttpOnly, SameSite из конфига (Lax, Strict или None; остальное отвергается), Secure автоматически для https. SameSite=None всегда ставит Secure и не принимает secure: false. «Для https» определяется по URI запроса: за прокси, снимающим TLS, это верно только с настроенным TrustedProxiesMiddleware, иначе задайте secure: true явно.

CookieSigner можно использовать отдельно: sign($value) → payload.signature, verify($cookie) → значение или null при неверной подписи; signature($payload) — одна подпись, для данных, которые передаются отдельно. Подходит для «remember me»; для подписанных ссылок есть UrlSigner на его основе. Давайте каждому применению своё назначение, new CookieSigner($secret, 'remember-me'): значение, подписанное для одного назначения, не пройдёт проверку под другим, так что id сессии никогда не сойдёт за remember-me-токен. Пустое назначение по умолчанию использует middleware сессий, им же подписывали предыдущие версии.

Инъекция по атрибуту работает для любого объекта: middleware кладёт объект в запрос под именем его класса ($request->withAttribute(Identity::class, $identity)), и параметр Identity $identity в хэндлере получает его без обращения к контейнеру.

Защита от CSRF

CsrfMiddleware, указанный в HttpConfig после SessionMiddleware, хранит в сессии synchronizer-токен (создаётся при первом запросе и живёт вместе с сессией) и оставляет на запросе CsrfToken. Запрос с небезопасным методом (всё кроме GET, HEAD, OPTIONS, TRACE) обязан повторить токен в поле формы _token или заголовке X-CSRF-Token, иначе получит 403 Invalid CSRF token.

// config/csrf.php (необязательно)
return new CsrfConfig(field: '_token', header: 'X-CSRF-Token', except: ['/api/']);

#[Get('/profile')]
public function form(CsrfToken $csrf): string
{
    return '<form method="post">' . $csrf->field() . '...</form>';   // скрытое поле
}

CsrfToken::$value уходит в заголовок fetch(); field() рендерит скрытое поле; CsrfMiddleware::token($request) читает токен из middleware. except перечисляет префиксы путей без проверки: API, который аутентифицируется bearer-токеном или API-ключом, недоступен с cookie браузера и в токене не нуждается. Чтобы сменить токен (например, после входа), удалите ключ сессии (_csrf по умолчанию) — следующий запрос получит новый. При cookie-хранилище токен едет внутри подписанной cookie, и это нормально: страница атакующего прочитать его не может, а только на этом проверка и держится.