Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Сесії

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

6 питань

Сесії зберігають стан користувача між запитами. Laravel дає єдиний API поверх різних драйверів (file, cookie, database, redis), що налаштовуються в config/session.php.

session(['cart_id' => 42]); // записати
$id = session('cart_id');   // прочитати
$request->session()->forget('cart_id'); // видалити
session()->flush();         // очистити все

Для продакшену з кількома серверами зазвичай обирають redis або database, щоб сесія була спільною між інстансами.

Докладніше в документації: Сесії

Flash Data - дані, що живуть у сесії лише до наступного запиту й потім автоматично видаляються. Класичне застосування - повідомлення про результат дії після редиректу.

return redirect('/dashboard')->with('status', 'Профіль оновлено!');
@if (session('status'))
    <div class="alert">{{ session('status') }}</div>
@endif

Під капотом - session()->flash('key', $value). Метод reflash() продовжує життя flash-даних ще на один запит.

Докладніше в документації: Flash-дані сесії

Драйвер задається в config/session.php. На одному сервері різниці майже немає, на кількох - вона вирішальна.

file - файли на диску сервера. За балансувальником користувач після кожного запиту може потрапити на інший сервер, де його сесії немає, тож його «розлогінює» через раз.

cookie - усе зберігається в самій куці, зашифрованій. Спільного сховища не треба, але обсяг обмежений ~4 КБ, і дані їздять у кожному запиті.

database - таблиця sessions, спільна для всіх серверів. Працює надійно, ціна - запит на кожен запит.

redis - спільне сховище в памʼяті. Стандартний вибір для кількох серверів: швидко й без навантаження на базу.

Що ще ламається на кількох серверах, крім драйвера:

  • APP_KEY має бути однаковий на всіх серверах, інакше куки, зашифровані одним, не розшифрує інший.
  • Той самий Redis мають бачити всі вузли - окремі інстанси не допоможуть.

Дві деталі, що псують життя окремо від масштабування. Сесія блокується на час запиту, тож кілька паралельних AJAX-запитів шикуються в чергу - для читання це знімається ->block() або сесією без запису. І в API сесій зазвичай узагалі не має бути: токен не потребує стану на сервері, а SESSION_DRIVER для stateless-запитів - зайва робота.

Докладніше в документації: HTTP-сесія

Laravel зчитує сесію на початку запиту, а записує цілком у кінці. Якщо два запити одного користувача виконуються паралельно, кожен працює зі своєю копією, і той, що завершиться останнім, перезапише зміни іншого.

Приклад:

Запит A (додати товар у кошик)   читає кошик [1]       записує [1, 2]
Запит B (додати інший товар)     читає кошик [1]       записує [1, 3]   <- товар 2 втрачено

Так буває з кількома AJAX-запитами одночасно, подвійним кліком, кількома вкладками. На відміну від стандартних файлових сесій PHP, драйвери Laravel (database, redis, file) не блокують сесію за замовчуванням.

Блокування сесії для конкретних маршрутів:

Route::post('/cart/items', AddToCartController::class)
    ->block($lockSeconds = 10, $waitSeconds = 10);
  • запит бере атомарне блокування для сесії (через кеш) і тримає його до $lockSeconds;
  • наступний запит цієї ж сесії до маршруту з block чекає до $waitSeconds, а якщо не дочекався - отримує LockTimeoutException;
  • запити виконуються послідовно, кожен бачить зміни попереднього.

Вимоги: драйвер кешу з атомарними блокуваннями (redis, memcached, database, dynamodb) і драйвер сесій не cookie.

Ціна: паралельність для одного користувача зникає - повільний запит затримує інші. Тому блокують лише маршрути, які змінюють сесію, а не весь застосунок.

Часто краще не тримати такі дані в сесії взагалі:

  • кошик, чернетки, налаштування - у базі з транзакціями чи атомарними операціями. Там конкурентність розв'язується надійніше й не залежить від сесії;
  • для лічильників - атомарний інкремент у Redis чи базі.

Як виявити проблему: «зникають» щойно додані дані, флеш-повідомлення показується не там, кошик втрачає товари при швидких кліках. Відтворюється одночасною відправкою двох запитів (наприклад, Promise.all з двох fetch).

Докладніше в документації: Блокування сесії

Фіксація сесії (session fixation): зловмисник отримує ідентифікатор сесії на сайті як гість і якимось чином змушує жертву користуватися саме ним (через вразливий піддомен, що встановлює cookie, через XSS чи спільний комп'ютер). Жертва входить - сесія з відомим зловмиснику ідентифікатором тепер автентифікована, і він заходить в акаунт.

Захист - новий ідентифікатор при зміні рівня привілеїв:

if (Auth::attempt($credentials, $request->boolean('remember'))) {
    $request->session()->regenerate();

    return redirect()->intended('/dashboard');
}

regenerate() видає новий ідентифікатор, зберігаючи дані сесії. Старий ідентифікатор більше нічого не дає. Starter kits і Fortify роблять це самі, але у власному контролері входу про це легко забути.

Вихід - три кроки:

Auth::logout();
$request->session()->invalidate();       // видалити дані й видати новий ідентифікатор
$request->session()->regenerateToken();  // новий CSRF-токен

return redirect('/');
  • Auth::logout() лише прибирає користувача з гарду - решта даних сесії (кошик, флеш, дані іншого гарду) лишається;
  • invalidate() очищує сесію і знищує запис у сховищі, тож старий ідентифікатор не можна використати;
  • regenerateToken() - щоб CSRF-токен з попередньої сесії не працював.

Інші моменти, де варто регенерувати:

  • підвищення привілеїв (вхід в адмін-режим, імперсонація й вихід з неї);
  • зміна пароля - разом з Auth::logoutOtherDevices($password), щоб завершити сесії на інших пристроях.

Що ще захищає сесію:

  • cookie з HttpOnly, Secure і SameSite=Lax - налаштування в config/session.php;
  • SESSION_DOMAIN без зайвої крапки: cookie для .example.com бачать усі піддомени, і вразливий піддомен стає точкою атаки;
  • обмежений lifetime і expire_on_close для чутливих застосунків;
  • для API з токенами (Sanctum) аналог - відкликання токенів при виході й зміні пароля.

Перевірка в тесті: ідентифікатор сесії після входу має відрізнятися від початкового - expect(session()->getId())->not->toBe($before).

Докладніше в документації: Регенерація ідентифікатора сесії

Драйвер database - за замовчуванням у нових застосунках:

  • таблиця sessions з user_id, IP і user agent - можна показати користувачу активні сесії й завершити їх;
  • не потрібна додаткова інфраструктура;
  • кожен запит пише в базу (оновлення last_activity), на навантаженому сайті це помітне навантаження;
  • очищення старих записів - через «лотерею»: 'lottery' => [2, 100] означає, що приблизно у 2 зі 100 запитів Laravel видаляє протерміновані сесії. На великій таблиці це раптово повільний запит у випадкового користувача.

Драйвер redis:

  • швидкий, не навантажує основну базу;
  • записи протерміновуються самим Redis через TTL - лотерея не потрібна (для драйверів на основі кешу очищення нічого не робить);
  • але немає зручного запиту «всі сесії користувача» - для такої функції доведеться зберігати зв'язок окремо;
  • налаштування Redis важливе: якщо в тому самому екземплярі кеш з політикою витіснення allkeys-lru, при нестачі пам'яті Redis видалятиме й сесії - користувачів почне розлогінювати. Сесії краще в окремій базі Redis чи екземплярі з noeviction.

Чого уникати:

  • file на кількох серверах - кожен сервер бачить лише свої сесії;
  • cookie для великих даних - ліміт розміру cookie близько 4 КБ і дані в кожному запиті;
  • array - лише для тестів.

Що не зберігати в сесії:

  • моделі й великі об'єкти - серіалізуються цілком, застарівають, роздувають кожен запит. Зберігайте ідентифікатори;
  • дані, що мають пережити вихід (кошик зареєстрованого користувача, налаштування) - у базі;
  • секрети й токени сторонніх сервісів без потреби: вміст сесії видно будь-кому з доступом до сховища (SESSION_ENCRYPT=true шифрує дані сесії);
  • дані, що змінюються паралельними запитами - без блокування вони перезаписують одне одного.

Ще кілька параметрів:

  • SESSION_LIFETIME - хвилини неактивності; для адмін-панелей коротше;
  • SESSION_CONNECTION - окреме з'єднання з базою чи Redis для сесій;
  • 'serialization' => 'json' у config/session.php замість PHP-серіалізації (за замовчуванням php) закриває клас атак через десеріалізацію, але вимагає зберігати в сесії лише прості значення.

Докладніше в документації: Налаштування сесій