Сессии
Сессия — это серверный набор данных на каждого пользователя, который сохраняется между запросами. Система сессий Lift:
- На основе драйверов — драйверы для файлов, базы данных, Redis, Memcached или в памяти.
- Управляется cookie — в cookie живёт только непрозрачный идентификатор сессии; данные остаются на сервере.
- Дружественна к PSR-15 — сессия предоставляется как атрибут запроса, так что ваши обработчики остаются тестируемыми.
Ментальная модель:
SessionMiddlewareчитает идентификатор сессии из cookie, загружает данные через подкладочное хранилище, прикрепляет объектSessionк запросу, затем записывает любые изменения обратно на выходе.
Простейшая настройка
Для прототипов — файловые сессии, база данных не требуется:
use Lift\App;
use Lift\Http\Session\FileSessionStore;
use Lift\Http\Session\Session;
use Lift\Http\Session\SessionMiddleware;
$app = new App();
$store = new FileSessionStore(__DIR__ . '/../storage/sessions');
$session = new Session($store, lifetime: 7200, cookieName: 'my_session');
$app->use(new SessionMiddleware($session));
// В любом обработчике:
$app->get('/me', function (Request $req) {
$session = $req->getAttribute('session'); // Lift\Http\Session\Session
return ['user_id' => $session->get('user_id')];
});
Убедитесь, что storage/sessions/ существует и доступен для записи пользователю веб-сервера.
Что делает middleware на каждый запрос
- Читает идентификатор сессии из cookie
my_session(или генерирует новый, если отсутствует). - Гидрирует сессию вызовом
Store::read($id). - Прикрепляет объект
Sessionк запросу как атрибут'session'. - Вызывает ваш обработчик.
- В блоке
finally: старит flash-данные, затем записывает черезStore::write($id, …, ttl). - Добавляет заголовок
Set-Cookie, чтобы браузер сохранил тот же идентификатор в следующий раз.
Даже когда ваш обработчик выбрасывает исключение, сессия всё равно сохраняется (шаг 5).
Чтение и запись
После прикрепления API Session мал и очевиден:
$session = $req->getAttribute('session');
$session->get('key', $default = null);
$session->set('key', $value);
$session->has('key'); // bool
$session->pull('key'); // get + delete за один вызов
$session->forget('key', 'another'); // удалить один или несколько ключей
$session->all(); // весь массив данных
Сцепляемо:
$session
->set('user_id', 42)
->set('last_seen', time());
Flash-сообщения
Flash-сообщение — это значение, которое живёт ровно один дополнительный запрос — идеально для уведомлений «действие успешно», показываемых после редиректа.
// В обработчике, который обработал POST формы:
$session->flash('notice', 'User created.');
return Response::redirect('/users');
// На странице /users после редиректа:
$notice = $session->pull('notice'); // 'User created.' при первом чтении, затем пропадает
Как это работает: flash() записывает значение обычным образом и помечает ключ в _flash_new. После выполнения обработчика ageFlashData() перемещает _flash_new → _flash_old, так что значение переживает ещё один запрос. При следующем вызове ageFlashData() всё в _flash_old удаляется.
Регенерация идентификатора сессии
Всегда регенерируйте идентификатор сессии сразу после изменения привилегий (вход, повышение роли), чтобы предотвратить атаки фиксации сессии:
$app->post('/login', function (Request $req) {
$session = $req->getAttribute('session');
// …аутентифицировать пользователя…
$session->regenerate(); // идентификатор ротирован, старая сессия удалена из хранилища
$session->set('user_id', $user->id);
return Response::redirect('/dashboard');
});
Передайте $deleteOldSession: false, если хотите сохранить старые данные доступными где-то ещё — почти никогда не правильный выбор.
Начиная с 1.2.1: как мера эшелонированной защиты, когда идентификатор сессии приходит из клиентской cookie, а хранилище не содержит сессии под ним,
start()чеканит свежий идентификатор вместо принятия предоставленного клиентом значения. Это не заменяет вызовregenerate()при входе — атакующий всё ещё может зафиксировать валидную сессию до аутентификации — но это останавливает прямое принятие фиксированного неизвестного идентификатора.
Уничтожение сессии (выход)
$app->post('/logout', function (Request $req) {
$req->getAttribute('session')->destroy();
return Response::redirect('/');
});
destroy() очищает данные и удаляет запись из хранилища.
Доступные драйверы
Все драйверы реализуют SessionStoreInterface. Выбирайте один в зависимости от того, где хотите хранить данные.
FileSessionStore
new FileSessionStore(__DIR__ . '/../storage/sessions');
Хранит один файл на идентификатор сессии. Подходит для одно-серверных, низконагруженных приложений. Запускайте периодическую задачу GC (store->gc(7200)), чтобы истёкшие файлы удалялись — или запускайте её встроенно в начале каждого запроса, если вам не важны несколько мс задержки.
DatabaseSessionStore
use Lift\Database\Connection;
use Lift\Http\Session\DatabaseSessionStore;
$db = Connection::fromConfig([...]);
// Создайте таблицу `sessions` один раз (или запустите `lift migrate`, если сгенерировали миграцию):
(new \Lift\Database\Migrator($db, '...'))->createSessionsTable();
new DatabaseSessionStore($db, table: 'sessions');
Переживает между серверами. Самый медленный из четырёх (каждое чтение/запись — это SQL-обращение).
RedisSessionStore
use Lift\Http\Session\RedisSessionStore;
use Lift\Redis\RedisClient;
$redis = new RedisClient(host: 'redis', port: 6379);
new RedisSessionStore($redis, prefix: 'sess:');
Нативный TTL, доступ за доли миллисекунды. Выбор по умолчанию для любого горизонтально масштабируемого развёртывания.
MemcachedSessionStore
new MemcachedSessionStore($memcached); // экземпляр ext-memcached
Как Redis, но использует Memcached. Не имеет персистентности — годится для сессий, но не для очередей.
ArraySessionStore
new ArraySessionStore();
Только в памяти, теряется при завершении процесса. Идеально для тестов.
Собственные хранилища
Реализуйте Lift\Http\Session\SessionStoreInterface:
interface SessionStoreInterface
{
public function read(string $id): ?string;
public function write(string $id, string $payload, int $ttl): void;
public function destroy(string $id): void;
public function gc(int $maxLifetime): void;
}
$payload — это непрозрачная PHP-сериализованная строка — ваше хранилище обращается с ней как с blob.
Атрибуты cookie
Когда middleware записывает cookie, он использует эти значения по умолчанию:
| Атрибут | По умолчанию | Переопределение |
|---|---|---|
Path |
/ |
жёстко прописан |
HttpOnly |
всегда | жёстко прописан |
SameSite |
Lax |
жёстко прописан |
Max-Age |
$lifetime (по умолчанию 7200 с) |
new Session($store, lifetime: …) |
Secure |
только по HTTPS | автоопределяется из $req->getUri()->getScheme() |
Если вам нужны другие атрибуты cookie (например, SameSite=Strict, родительский домен и т. д.), создайте собственный middleware или унаследуйте SessionMiddleware.
Чек-лист безопасности
- ✅ Всегда используйте HTTPS в продакшене. Cookie сессии — самая критичная для безопасности часть вашего стека.
- ✅ Вызывайте
$session->regenerate()при входе / изменении привилегий. - ✅ Вызывайте
$session->destroy()при выходе. - ✅ Для чувствительных данных не кладите их в сессию — только непрозрачный идентификатор пользователя. Остальное ищите на сервере на каждом запросе.
- ✅ Задайте разумный
lifetime. 2 часа — по умолчанию; 30 минут безопаснее для админ-областей.
Начиная с 1.3.0: десериализация объектов отключена по умолчанию (
allowedClasses: false). Это не даёт подделанным или устаревшим payload'ам сессии инстанцировать классы приложения.
- ❌ Не сериализуйте объекты с секретами в сессию — передайте белый список разрешённых классов или храните только идентификаторы:
new Session($store); // объектов нет по умолчанию new Session($store, allowedClasses: [Money::class]); // явный список разрешённых
Частые подводные камни
| Симптом | Причина | Исправление |
|---|---|---|
| Сессия пуста на каждом запросе | Middleware не зарегистрирован или неверное имя cookie | $app->use(new SessionMiddleware($session)); и проверьте cookieName. |
| Вход работает локально, но не в продакшене | Установлен флаг Secure cookie, но вы на HTTP |
Используйте HTTPS или настройте обратный прокси с терминацией TLS. |
| Данные теряются между двумя серверами | Файловое хранилище + несколько серверов приложения | Перейдите на Redis/БД. |
Предупреждения безопасности unserialize |
Вы сохранили объект, чей класс больше не загружаем | Используйте allowedClasses: false и храните только скаляры. |
| Flash-сообщение не появляется | Вы вызвали flash(), затем прочитали его на том же запросе |
Flash для следующего запроса — сначала редирект, затем чтение. |
| Сессия «разлогинена» при POST | CSRF-middleware регенерировал идентификатор; или вы переиспользовали старую ссылку $session после regenerate() |
Перечитайте через $req->getAttribute('session') после чувствительных изменений. |
Шпаргалка
// Загрузка
$store = new FileSessionStore($path); // или Redis/БД/Memcached
$session = new Session($store, lifetime: 7200);
$app->use(new SessionMiddleware($session));
// Использование
$session = $req->getAttribute('session');
$session->set('user_id', 42);
$session->get('user_id');
$session->pull('flash');
$session->flash('notice', 'OK');
$session->regenerate(); // после входа
$session->destroy(); // при выходе