Сесії
Сесія — це серверний набір даних на кожного користувача, який зберігається між запитами. Система сесій 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(); // під час виходу