Статьи

Сохранение сессий PHP в Redis

Сеансы позволяют веб-приложению поддерживать состояние нескольких HTTP-запросов. Вы можете зарегистрировать любое количество переменных в качестве переменных сеанса, которые затем обычно хранятся во временном каталоге на сервере или передаются в браузер клиента через файлы cookie. Последующие запросы имеют доступ к сохраненной информации, и состояние приложения сохраняется в нескольких запросах.

Обработка данных сеанса в PHP по умолчанию, вероятно, достаточна для большинства приложений, но иногда проект требует другого подхода к хранению. К счастью, подпрограммы обработки сеансов могут быть переопределены либо рядом функций (в более старых версиях PHP), либо классом с методами (в более новых версиях), которые обрабатывают различные аспекты управления сеансами.

В этой статье мы узнаем, как создать собственный обработчик сеанса, который реализует интерфейс PHP SessionHandlerInterface и сохраняет данные сеанса в базе данных Redis.

Почему нестандартное хранилище?

Вы можете задаться вопросом, почему кто-то может захотеть (или должен) пройти через процесс написания собственного обработчика. Если это так, может быть полезно рассмотреть следующее.

Обычно приложения располагаются в ферме серверов для каждого хоста для обработки запросов на балансировку нагрузки и избыточность. Данные сеанса, хранящиеся во временных файлах, будут доступны только для их конкретного хоста. Поскольку сервер, обрабатывающий запрос, может не совпадать с сервером, обработавшим предыдущий запрос, гарантированный доступ к информации о состоянии невозможен без альтернативного механизма обработки. Данные сеанса могут храниться в центральном механизме хранения и быть доступными для всех машин в кластере.

На сервере общего хостинга вполне вероятно, что все временные файлы сеансов хранятся в одном и том же каталоге. Каталог и его содержимое доступны для любых процессов, инициированных с использованием учетной записи веб-службы, которые могут представлять серьезную угрозу безопасности. Вредоносный пользователь, который может получить доступ к чужим данным сеанса, имеет возможность выдавать себя за законного пользователя.

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

Действия по управлению сессиями

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

  • open — открывает ресурс для механизма хранения, предназначенного для сохранения информации о сеансе.
  • закрыть — закрывает ресурс хранилища и инициирует любые другие необходимые действия по очистке.
  • чтение — извлекает ранее сохраненные данные сеанса из механизма хранения.
  • write — сохраняет новые данные сеанса, которые приложение должно запомнить.
  • уничтожить — сбрасывает сеанс и удаляет любую информацию из него.
  • gc — удаляет данные из механизма хранения после того, как они устарели и больше не нужны.

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

В версиях PHP до 5.4 нам нужно было бы указать шесть вызываемых объектов (одну функцию или метод для обработки каждой задачи) в указанном выше порядке в качестве аргументов функции session_set_save_handler() . В 5.4 и более поздних версиях мы можем создать класс, который реализует интерфейс SessionHandlerInterface и вместо этого передать экземпляр. Интерфейс предоставляет шесть методов с теми же сигнатурами, которые будут использоваться для функций в подходе до 5.4.

Механизм хранения

Ваш выбор механизма хранения диктуется вашими потребностями. Это может быть что угодно — удаленные временные файлы, база данных MySQL, сервер LDAP, сегменты разделяемой памяти, служба XML-RPC, папка входящих сообщений IMAP и т. Д. В этой статье я собираюсь проиллюстрировать хранение данных сеанса в Redis.

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

Данные сеанса также автоматически сериализуются и не сериализуются PHP. То есть, способ, который получает данные для хранения, передает уже сериализованные данные, а метод, который извлекает данные, должен возвращать сериализованные данные. Функции session_encode() и session_decode() также доступны, если они нам нужны по какой-либо причине, но обычно мы можем просто сохранять и извлекать данные сеанса, как предусмотрено.

Конечно, важно очистить устаревшие сеансы, которые больше не нужны. Например, если мы храним данные сеанса, например, в базе данных MySQL, мы бы хотели включить метку времени в каждую запись. Отметка времени будет проверена нашим методом, который переопределяет поведение сборки мусора. Но с Redis мы можем воспользоваться его командой EXPIRE . EXPIRE устанавливает тайм-аут или TTL (время жизни) для ключа, и ключ автоматически удаляется после истечения времени ожидания.

Покажи мне код!

Теперь мы знаем, какую функциональность SessionHandlerInterface интерфейс SessionHandlerInterface , и у нас есть SessionHandlerInterface представление о том, как методы должны взаимодействовать с Redis, мы можем написать наш код. Без лишних слов, вот класс:

 <?php class RedisSessionHandler implements SessionHandlerInterface { public $ttl = 1800; // 30 minutes default protected $db; protected $prefix; public function __construct(PredisClient $db, $prefix = 'PHPSESSID:') { $this->db = $db; $this->prefix = $prefix; } public function open($savePath, $sessionName) { // No action necessary because connection is injected // in constructor and arguments are not applicable. } public function close() { $this->db = null; unset($this->db); } public function read($id) { $id = $this->prefix . $id; $sessData = $this->db->get($id); $this->db->expire($id, $this->ttl); return $sessData; } public function write($id, $data) { $id = $this->prefix . $id; $this->db->set($id, $data); $this->db->expire($id, $this->ttl); } public function destroy($id) { $this->db->del($this->prefix . $id); } public function gc($maxLifetime) { // no action necessary because using EXPIRE } } с <?php class RedisSessionHandler implements SessionHandlerInterface { public $ttl = 1800; // 30 minutes default protected $db; protected $prefix; public function __construct(PredisClient $db, $prefix = 'PHPSESSID:') { $this->db = $db; $this->prefix = $prefix; } public function open($savePath, $sessionName) { // No action necessary because connection is injected // in constructor and arguments are not applicable. } public function close() { $this->db = null; unset($this->db); } public function read($id) { $id = $this->prefix . $id; $sessData = $this->db->get($id); $this->db->expire($id, $this->ttl); return $sessData; } public function write($id, $data) { $id = $this->prefix . $id; $this->db->set($id, $data); $this->db->expire($id, $this->ttl); } public function destroy($id) { $this->db->del($this->prefix . $id); } public function gc($maxLifetime) { // no action necessary because using EXPIRE } } 

Первое, что вы можете заметить, это то, что методы open() и gc() пусты. Я использовал внедрение зависимостей для предоставления соединения Redis, которое открывает класс для модульного тестирования, поэтому ничего не нужно делать в open() . В gc() ничего не нужно делать, потому что Redis будет обрабатывать устаревшие устаревшие ключи для нас.

В дополнение к соединению Redis конструктор также принимает префикс. Префикс и идентификатор сеанса, сгенерированные PHP, объединяются и используются в качестве ключа для хранения и извлечения значений. Это в первую очередь средство предотвращения конфликтов имен, но также дает то преимущество, что мы будем знать, на что мы смотрим, если мы выполним KEYS * в клиенте Redis.

Когда PHP вызывает метод write() , он передает два значения: идентификатор сеанса и сериализованную строку данных сеанса. Команда SET используется для хранения данных в Redis, и мы касаемся TTL ключа. Все записи, помещенные в $_SESSION массив $_SESSION , теперь сохраняются.

Когда PHP вызывает метод read() , он передает идентификатор сеанса. Команда GET используется для извлечения данных, и мы также снова касаемся TTL — в конце концов, если мы обращаемся к сеансу, то имеет смысл считать его все еще свежим. Обратите внимание, что данные сеанса возвращаются в его сериализованной форме непосредственно из хранилища. PHP получает строку, десериализует ее и заполняет массив $_SESSION .

Использовать наш обработчик так же просто, как создать экземпляр и передать его в session_set_save_handler() .

 <?php $db = new PredisClient(); $sessHandler = new RedisSessionHandler($db); session_set_save_handler($sessHandler); session_start(); 

Когда вызывается session_start() , PHP будет использовать наш собственный обработчик для управления сессиями вместо подхода по умолчанию; никаких других изменений в нашем коде не требуется.

Если вы застряли на версии PHP более ранней, чем 5.4, вы все равно можете использовать приведенный выше класс (хотя вам придется либо насмехаться над SessionHandlerInterface либо полностью удалить часть implements ). Его регистрация будет выглядеть так:

 <?php $db = new PredisClient(); $sessHandler = new RedisSessionHandler($db); session_set_save_handler( array($sessHandler, 'open'), array($sessHandler, 'close'), array($sessHandler, 'read'), array($sessHandler, 'write'), array($sessHandler, 'destroy'), array($sessHandler, 'gc') ); session_start(); 

Вывод

В этой статье мы увидели, как легко реализовать интерфейс SessionHandlerInterface и предоставить логику, необходимую PHP для хранения данных сеанса в базе данных Redis. Пользовательская обработка сессий прозрачна на уровне кода, и в результате вы получаете удивительный способ повысить безопасность и гибкость своего приложения без особых усилий! Для получения дополнительной информации прочитайте, что говорит руководство PHP о пользовательской обработке сеансов , и изучите сопровождающий код для этой статьи на GitHub.

Изображение через Fotolia