Перейти к основному содержимому

Сессии в PHP: Глубокое погружение

Поддержка сессий в PHP — это мощный механизм сохранения состояния между несколькими запросами. Понимание внутренних процессов работы сессий критически важно для создания масштабируемых и безопасных приложений и является частой темой на собеседованиях уровня Middle и Senior.

1. Внутренний жизненный цикл​

Сессия в PHP проходит через строгий, хорошо определенный жизненный цикл. Понимание этого процесса необходимо для отладки таких проблем, как потеря данных сессии или блокировка параллельных запросов.

Этап 1: Начало запроса и идентификация​

  • Сторона клиента: Когда браузер делает запрос, он проверяет свои куки. Если он находит куку, соответствующую имени сессии (по умолчанию PHPSESSID) и текущему домену/пути, он отправляет её в заголовке Cookie.
  • Сторона сервера: PHP получает запрос. Если включена опция session.auto_start (не рекомендуется), PHP немедленно запускает жизненный цикл. В противном случае он ожидает явного вызова session_start().

Этап 2: Инициализация (session_start())​

Это самый критический этап, на котором последовательно происходит несколько действий:

  1. Получение ID сессии: PHP ищет ID в запросе. Если он не найден, генерируется новая криптографически стойкая случайная строка.
  2. Открытие обработчика хранения: PHP вызывает метод open настроенного обработчика (например, открывает файл или подключается к Redis).
  3. Блокировка (Exclusive Lock): При использовании обработчика files, PHP выполняет flock() (эксклюзивную блокировку) файла сессии. Это гарантирует, что только один скрипт одновременно может иметь доступ к данным сессии конкретного пользователя.
  4. Чтение и десериализация: PHP читает содержимое хранилища сессий. Затем он использует session.serialize_handler для преобразования строки обратно в массив PHP и заполняет суперглобальный массив $_SESSION.
  5. Управление куками: Если была начата новая сессия, PHP добавляет заголовок Set-Cookie в буфер ответа, чтобы дать браузеру команду сохранить ID сессии.

Этап 3: Выполнение скрипта​

  • Теперь разработчик может свободно читать и записывать данные в $_SESSION.
  • Важно: Изменения, внесенные в массив, на данном этапе находятся только в оперативной памяти (RAM). Они еще не сохранены в постоянное хранилище.

Этап 4: Завершение и сохранение (Commit)​

Сессия сохраняется при завершении работы скрипта или при явном вызове session_write_close():

  1. Сериализация: PHP берет текущее состояние $_SESSION и преобразует его в сериализованную строку.
  2. Запись: Сериализованная строка записывается в хранилище (например, перезаписывается файл сессии или обновляются данные в Redis).
  3. Разблокировка: Эксклюзивная блокировка снимается, что позволяет другим ожидающим запросам от того же пользователя продолжить выполнение.
  4. Закрытие обработчика хранения: Вызывается метод close обработчика сохранения.

Этап 5: Сборка мусора (Вероятностная)​

До или после обработки сессии PHP может запустить сборщик мусора (Garbage Collector, GC):

  • Он проверяет session.gc_maxlifetime и сравнивает его с временем последнего изменения файлов сессий.
  • Он запускается только в том случае, если выигрывает в «лотерею» на основе session.gc_probability / session.gc_divisor.

Совет эксперта: Используйте session_write_close() сразу после того, как закончили запись в сессию. Это освободит блокировку раньше и позволит параллельным запросам от того же пользователя выполняться, не дожидаясь завершения первого.

2. Конкурентность и блокировки​

По умолчанию сессии в PHP являются блокирующими. Если пользователь откроет три вкладки, которые вызывают session_start(), вторая и третья вкладки будут «висеть» до тех пор, пока первая не завершится (или не вызовет session_write_close()).

  • Влияние: Запросы с большой задержкой (например, long-polling или тяжелые отчеты) могут заблокировать работу всего интерфейса для пользователя.
  • Решение: Используйте session_write_close() для операций, где требуется только чтение, или перейдите на неблокирующее хранилище, такое как Redis (хотя некоторые обработчики Redis также реализуют блокировки).

3. Хранение и сериализация​

Обработчики хранения (session.save_handler)​

  • files: По умолчанию. Просто, но не масштабируется на несколько серверов.
  • redis / memcached: Рекомендуется для продакшена. Позволяет нескольким веб-серверам совместно использовать данные сессий.
  • user: Позволяет реализовать SessionHandlerInterface для хранения сессий где угодно (например, в SQL базе данных).

Обработчики сериализации (session.serialize_handler)​

  • php: Внутренний формат PHP. Плохо справляется с числовыми ключами или спецсимволами в ключах.
  • php_binary: Более компактный бинарный формат.
  • php_serialize: Рекомендуется. Использует стандартные функции serialize()/unserialize(). Самый надежный формат, лучше всего обрабатывает сложные структуры данных.

Важно: Никогда не храните Ресурсы (Resources), такие как соединения PDO, дескрипторы файлов или изображения GD, в сессиях. Они не могут быть сериализованы и будут потеряны или испорчены.

4. Сборка мусора (Garbage Collection, GC)​

Так как сессии не «разлогиниваются» автоматически, PHP должен очищать старые файлы сессий. Это реализовано через вероятностный механизм:

  • session.gc_maxlifetime: Время в секундах, после которого данные считаются «мусором» (по умолчанию 1440 сек / 24 мин).
  • session.gc_probability / session.gc_divisor: Вероятность запуска сборщика мусора. По умолчанию это 1 / 1000 (0.1% шанс при каждом вызове session_start()).

Примечание: В некоторых системах (например, Debian/Ubuntu) вероятность установлена в 0, и очисткой директории /var/lib/php/sessions занимается системный cron-задача.

5. Безопасность​

Session Fixation (Фиксация сессии)​

Атакующий передает жертве известный ему ID сессии (например, через ссылку).

  • Защита: Вызывайте session_regenerate_id(true) сразу после входа пользователя в систему. Это сменит ID и удалит старый.
  • Строгий режим: Включите session.use_strict_mode = 1. Это запретит PHP принимать неинициализированные ID сессий, присланные клиентом.

Session Hijacking (Угон сессии)​

Атакующий крадет валидный ID сессии (например, через XSS или перехват трафика).

  • Защита:
    • Используйте HTTPS и установите session.cookie_secure = 1.
    • Установите session.cookie_httponly = 1, чтобы запретить доступ через JavaScript.
    • Установите session.cookie_samesite = "Lax" или "Strict" для защиты от CSRF.

Настройки (php.ini)​

session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.gc_maxlifetime = 3600
session.use_only_cookies = 1

6. «Шпаргалка» для собеседования​

  1. Как справиться с тем, что параллельные запросы блокируют друг друга?
    • Использовать session_write_close() как можно раньше.
  2. Почему нельзя хранить соединение с базой данных в сессии?
    • Ресурсы нельзя сериализовать; они представляют собой живое соединение, которое нельзя «заморозить» и «разморозить» в виде строки.
  3. Как разделить сессии между двумя разными поддоменами?
    • Установить session.cookie_domain = ".example.com".
  4. Что произойдет, если вызвать session_start() после вывода данных?
    • Произойдет ошибка "headers already sent", так как функция пытается отправить куку сессии в HTTP-заголовке.
  5. Как масштабировать сессии горизонтально?
    • Использовать централизованное хранилище, такое как Redis или Memcached, вместо локальной файловой системы.