Класс SessionHandler
SessionHandler это специальный класс, который может использоваться для дополнения внутреннего обработчика сессий PHP путём создания дочерних классов от этого. Существует семь методов, которые являются обёртками над семью внутренними обработчиками хранения данных сессии ( open , close , read , write , destroy , gc и create_sid ). По умолчанию этот класс оборачивает все внутренние обработчики хранения сессии, определённые в опции конфигурации session.save_handler. Эта опция по умолчанию имеет значение files . Другие внутренние обработчики сессий предоставляются PHP-модулями, такими как SQLite ( sqlite ), Memcache ( memcache ) и Memcached ( memcached ).
Экземпляр класса SessionHandler может устанавливаться в качестве обработчика сессии посредством вызова функции session_set_save_handler() . В этом случае он станет обёрткой существующего внутреннего обработчика. Классы, расширяющие SessionHandler позволят переопределить методы обработчика сессии или перехватить/отфильтровать их путём вызова родительских методов-обёрток внутреннего обработчика сессий PHP.
Это позволит вам, к примеру, перехватить методы read и write для шифровки/дешифровки данных сессии и передачи результата родительскому классу и от него. Или, к примеру, вы можете полностью переопределить такой метод как callback-функция сборщика мусора ( gc ).
Так как SessionHandler является обёрткой над стандартным внутренним обработчиком сессии, то пример, приведённый выше про шифровку данных может быть применён к любому внутреннему обработчику сессии даже без понимания внутреннего устройства процесса работы сессии.
Для использования этого класса, во-первых, установите обработчик, который вы хотите дополнить используя session.save_handler. Далее передайте экземпляр класса SessionHandler или одного из классов, расширяющих его функции session_set_save_handler() .
Обратите внимание, что callback-методы этого класса предназначены для внутреннего вызова PHP и не предназначены для вызова из кода пользовательского пространства. Возвращаемые значения одинаково обрабатываются внутри PHP. Дополнительную информацию о работе с сессией можно узнать из описания функции session_set_save_handler() .
Работа с сессиями (сеансами) в PHP 7
В данной заметке коротко о том, как использовать сеансы (сессии) в PHP, как они работают и как они связаны с файлами cookie (куки).
Что такое сессия в PHP?
Сессия — это механизм для сохранения информации на разных веб-страницах для идентификации пользователей при навигации по сайту или приложению. Сессия PHP представляет собой способ хранения информации в переменных сессии, которые могут быть использованы для аутентификации на нескольких веб-страницах. В отличие от куков, информация сессии не хранится на компьютере пользователя. Вместо этого сессия создает файл на сервере во временном каталоге.
Рекомендуется настроить путь для хранения сессий на сервере. Сделать это можно несколькими способами. Необходимо заранее создать указанные директории!
session.save_path = "/path" // Моя настройка в Windows 10 session.save_path = "S:\Server\session"
В файле .htaccess :
php_value session.save_path "/path" // Моя настройка в Windows 10 php_value session.save_path "S:\Server\session"
Выше варианты для глобальной конфигурации хранения сессий. Можно настраивать хранение сессий для каждого проекта отдельно. Для этого в проекте необходимо вызвать php функцию session_save_path() c указанием необходимого пути. Перед session_start() укажем путь для хранения сессий проекта:
session_save_path("S:\Server\session"); session_start();
ini_set('session.save_path', "S:\Server\session"); session_start();
Создание сессии (сеанса) в PHP
Для того, чтобы начать сеанс (запустить сессию), необходимо вызвать функцию session_start() . Данную функцию нужно запускать вначале каждой страницы, на которой мы хотим в дальнейшем прочитать данные из сессии.
При каждом вызове session_start() PHP сам отправляет куку в браузер пользователя.
// Синтаксис session_start ([ array $options = array() ] ) : bool // Пример session_start ();
С помощью аргументов (ассоциативный масс $options ) можно переопределить текущие директивы конфигурации сессий, которые указаны в php.ini . Список всех директив: здесь
Запись и чтение сессий
После того, как на сервере создался файл вида sess_. , в данную сессию можно записать информацию, а так же считать её от туда.
// Запись в сессионную переменную $_SESSION['name'] = 'John'; // Чтение из сессионной переменной echo $_SESSION['name'];
Завершение (очистка) сессии
Так как сессии часто используются для авторизации пользователей, то нужно иметь механизм «выхода» из системы. Для того, чтобы завершить сессию нам нужно:
- Очистить массив $_SESSION
- Удалить временное хранилище (файл сессии) на сервере
- Удалить сессионную куку (cookie).
// Очистить массив $_SESSION полностью session_unset(); // Удалить сессионную переменную unset($_SESSION['age']); // Удалить временное хранилище (файл сессии) на сервере session_destroy(); // Принудительное удаление сессионной cookie setcookie(session_name(), session_id(), time()-3600);
- session_id() — уникальный идентификатор сессии
- session_name() — значение идентификатора сессии
Итоги
// Создание и(или) доступ к сессии session_start(); // Запись в сессионную переменную $_SESSION['name'] = 'John'; // Чтение из сессионной переменной echo $_SESSION['name']; // Удалить сессионную переменную unset($_SESSION['age']); // Очистить массив $_SESSION полностью session_unset(); // Удалить временное хранилище (файл сессии) на сервере session_destroy(); // Принудительное удаление сессионной cookie setcookie(session_name(), session_id(), time()-3600); // Простой пример использования сессий session_save_path('С:\temp'); session_start(); if (!isset($_SESSION['counter'])) $_SESSION['counter'] = 0; echo "Вы обновили эту страницу ".$_SESSION['counter']++." раз. "; echo "
обновить";
Пример использования сессии
В данном примере я покажу как можно использовать сессии (сеансы) в PHP на практике. Для начала на сервере создадим файл index.php со следующим содержимым:
Сессии Создание сессии
Закрыть сессию
" method="post"> Ваше имя:
">
Ваш возраст:
">
Привет, $name!"; echo "
Тебе $age лет
"; > ?>
Здесь я постарался максимально понятно описать всё происходящее. Под пользователя при заходе на эту страницу уже создаётся сессия, она пустая, это пока просто файл. Если пользователь заполнил все необходимые поля формы и нажал кнопку `Отправить`, то созданный файл (файл сессии) заполниться данными. Далее скрипт проверяет, если есть в сессии данные именно этого пользователя (здесь он уже сравнивает с кукой, которая пришла до этого на машину пользователя), то выводим эти данные. В противном случае, добавляем в сессию данные пользователя.
Создадим файл destroy.php . При переходе на данную страницу будем удалять сессию полностью. Если необходимо удалять и сессионную куку, воспользуйтесь функцией:
setcookie(session_name(), session_id(), time()-3600);
// Файл destroy.phpЗакрыть сессию Сессия закрыта
Создать сессию
Как изменить папку для хранения файлов сессий
На первом шаге установки Joomla, при проверке параметров сервера, иногда выдается ошибка, что папка указанная в переменной Session save path недоступна по записи. Конечно это не помешает установить Joomla, но создаст серьезные проблемы при попытке авторизации в административной панели. Что делать, если на сервере, по каким либо причинам неверно указан путь к папке хранения файлов сессий или отсутствуют права на запись?
Если папка существует и проблема в отсутствии прав на запись, то в первую очередь следует попробовать установить соответствующие права. Если же это по каким либо причинам невозможно, то можно попробовать изменить значение этой переменной, указав в ней путь к другой существующей и доступной на запись папке на серрве. То есть создать отдельную папку, и изменить значение переменной PHP session.save_path одним из ниже перечисленных способов.
Изменение параметра session.save_path в php.ini
Изменить папку для хранения файлов сессий можно в файле php.ini (конфигурационный файл PHP). Для этого необходимо найти в файле php.ini параметр session.save_path и в качестве значения параметра указать существующую и доступную по записи папку. Например вот так:
session.save_path = d:/www/tmp/
где «d:/www/tmp/» это абсолютный путь к существующей и доступной для записи папке
Использование .htaccess
Изменить директорию для хранения файлов сессий можно добавив в файл .htaccess, расположенный в корне сайта, следующую строчку:
php_value session.save_path "d:/www/tmp/"
где «d:/www/tmp/» это абсолютный путь к существующей и доступной для записи папке
Использование функции session_save_path
Необходимо в файлах:
- offline.php
- administrator\index.php
- administrator\index2.php
- administrator\index3.php
- administrator\components\com_users\admin.users.php
- administrator\includes\auth.php
session_start();
и перед ней добавить:
session_save_path("d:/www/tmp/");
где «d:/www/tmp/» это абсолютный путь к существующей и доступной для записи папке
Использование функции ini_set
Необходимо в файлах:
- offline.php
- administrator\index.php
- administrator\index2.php
- administrator\index3.php
- administrator\components\com_users\admin.users.php
- administrator\includes\auth.php
session_start();
и перед ней добавить:
ini_set('session.save_path', "d:/www/tmp/");
Хранение php-сессий в Redis с блокировками
Стандартный механизм хранения данных пользовательских сессий в php — хранение в файлах. Однако при работе приложения на нескольких серверах для балансировки нагрузки, возникает необходимость хранить данные сессий в хранилище, доступном каждому серверу приложения. В этом случае для хранения сессий хорошо подходит Redis.
Наиболее популярное решение — расширение phpredis. Достаточно установить расширение и настроить php.ini и сессии будут автоматически сохраняться в Redis без изменения кода приложений.
Однако такое решение имеет недостаток — отсутствие блокировки сессии.
При использовании стандартного механизма хранения сессий в файлах открытая сессия блокирует файл пока не будет закрыта. При нескольких одновременных обращениях доступ к сессии новые запросы будут ожидать, пока предыдущий не завершит работу с сессией. Однако при использовании phpredis подобного механизма блокировок нет. При нескольких асинхронных запросов одновременно происходит гонка, и некоторые данные, записываемые в сессию, могут быть утеряны.
Это легко проверить. Отправляем на сервер асинхронно 100 запросов, каждый из которых пишет в сессию свой параметр, затем считаем количество параметров в сессии.
Тестовый скрипт
'; break; >
В результате получаем, что в сессии не 100 параметров, а 60-80. Остальные данные мы потеряли.
В реальных приложениях конечно 100 одновременных запросов не будет, однако практика показывает, что даже при двух асинхронных одновременных запросах данные, записываемые одним из запросов, довольно часто затираются другим. Таким образом, использование расширения phpredis для хранения сессий небезопасно и может привести к потере данных.
Как один из вариантов решения проблемы — свой SessionHandler, поддерживающий блокировки.
Реализация
Чтобы установить блокировку сессии, установим значение ключа блокировки в случайно сгенерированное (на основе uniqid) значение. Значение должно быть уникальным, чтобы любой параллельный запрос не мог получить доступ.
protected function lockSession($sessionId) < $attempts = (1000000 * $this->lockMaxWait) / $this->spinLockWait; $this->token = uniqid(); $this->lockKey = $sessionId . '.lock'; for ($i = 0; $i < $attempts; ++$i) < $success = $this->redis->set( $this->getRedisKey($this->lockKey), $this->token, [ 'NX', ] ); if ($success) < $this->locked = true; return true; > usleep($this->spinLockWait); > return false; >
Значение устанавливается с флагом NX, то есть установка происходит только в случае, если такого ключа нет. Если же такой ключ существует, делаем повторную попытку через некоторое время.
Можно также использовать ограниченное время жизни ключа в редисе, однако время работы скрипта может быть изменено после установки ключа, и параллельный процесс сможет получить доступ к сессии до завершения работы с ней в текущем скрипте. При завершении работы скрипта ключ в любом случае удаляется.
При разблокировке сессии при завершении работы скрипта для удаления ключа используем Lua-сценарий:
private function unlockSession() < $script = redis->eval($script, array($this->getRedisKey($this->lockKey), $this->token), 1); $this->locked = false; $this->token = null; >
Использовать команду DEL нельзя, так как с помощью нее можно удалить ключ, установленный другим скриптом. Такой сценарий же гарантирует удаление только в случае, если ключу блокировки соответствует уникальное значение, установленное текущим скриптом.
Полный код класса
class RedisSessionHandler implements \SessionHandlerInterface < protected $redis; protected $ttl; protected $prefix; protected $locked; private $lockKey; private $token; private $spinLockWait; private $lockMaxWait; public function __construct(\Redis $redis, $prefix = 'PHPREDIS_SESSION:', $spinLockWait = 200000) < $this->redis = $redis; $this->ttl = ini_get('gc_maxlifetime'); $iniMaxExecutionTime = ini_get('max_execution_time'); $this->lockMaxWait = $iniMaxExecutionTime ? $iniMaxExecutionTime * 0.7 : 20; $this->prefix = $prefix; $this->locked = false; $this->lockKey = null; $this->spinLockWait = $spinLockWait; > public function open($savePath, $sessionName) < return true; >protected function lockSession($sessionId) < $attempts = (1000000 * $this->lockMaxWait) / $this->spinLockWait; $this->token = uniqid(); $this->lockKey = $sessionId . '.lock'; for ($i = 0; $i < $attempts; ++$i) < $success = $this->redis->set( $this->getRedisKey($this->lockKey), $this->token, [ 'NX', ] ); if ($success) < $this->locked = true; return true; > usleep($this->spinLockWait); > return false; > private function unlockSession() < $script = redis->eval($script, array($this->getRedisKey($this->lockKey), $this->token), 1); $this->locked = false; $this->token = null; > public function close() < if ($this->locked) < $this->unlockSession(); > return true; > public function read($sessionId) < if (!$this->locked) < if (!$this->lockSession($sessionId)) < return false; >> return $this->redis->get($this->getRedisKey($sessionId)) ?: ''; > public function write($sessionId, $data) < if ($this->ttl > 0) < $this->redis->setex($this->getRedisKey($sessionId), $this->ttl, $data); > else < $this->redis->set($this->getRedisKey($sessionId), $data); > return true; > public function destroy($sessionId) < $this->redis->del($this->getRedisKey($sessionId)); $this->close(); return true; > public function gc($lifetime) < return true; >public function setTtl($ttl) < $this->ttl = $ttl; > public function getLockMaxWait() < return $this->lockMaxWait; > public function setLockMaxWait($lockMaxWait) < $this->lockMaxWait = $lockMaxWait; > protected function getRedisKey($key) < if (empty($this->prefix)) < return $key; >return $this->prefix . $key; > public function __destruct() < $this->close(); > >
Подключение
$redis = new Redis(); if ($redis->connect('11.111.111.11', 6379) && $redis->select(0)) < $handler = new \suffi\RedisSessionHandler\RedisSessionHandler($redis); session_set_save_handler($handler); >session_start();
Результат
После подключения нашего SessionHandler наш тестовый скрипт уверенно показывает 100 параметров в сессии. При этом несмотря на блокировки общее время обработки 100 запросов выросло незначительно. В реальной практике такого количества одновременных запросов не будет. Однако время работы скрипта обычно более существенно, и при одновременных запросах может быть заметное ожидание. Поэтому нужно думать о сокращении времени работы с сессией скрипта (вызове session_start() только при необходимости работы с сессией и session_write_close() при завершении работы с ней)
