Сессии и подписанные 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, и это нормально: страница атакующего прочитать его не может, а
только на этом проверка и держится.