Использование MySQL для хранения данных сессий

Для высоко нагруженных проектов использование файлов для хранения файлов сессий становится недопустимым.

В этой статье мы рассмотрим использвоание БД MySql для хранения данных сессий.

  1. все настройки сессий производятся до старта сессии, поэтому необходимо отменить автостарт сессий:
    ini_set('session.auto_start', '0');
    

  2. стандартно PHP хранит сессии в файлах, чтобы установить свои обработчики сессий определим:
    ini_set('session.save_handler', 'user');
    
    у 'session.save_handler' может быть три значения:
    • files - значение по умолчанию, PHP использует стандартные функции обработки сессий, сессии храняться в файлах, необходимо определить ini_set('session.save_path', путь); место для хранения файлов сессий.;
    • mm - PHP использует стандартные функции обработки сессий, сессии храняться в памяти;
    • user - позволяет переопределять стандартные функции обработки сессий, и соответственно в этих функциях указывать, где мы будем хранить сессии и как мы будем их обрабатывать.

  3. теперь определим функции обработки сессий:
    session_set_save_handler (
            "sess_open",
            "sess_close",
            "sess_read",
            "sess_write",
            "sess_destroy",
            "sess_gc");
    рассмотрим каждую:
    • sess_open - открывает сессию. Функция создает уникальное ID сессии. Требует для своей работы два параметра 'session.save_path' и 'session.name'. Т.к. мы храним сессии в базе, то 'session.save_path' нам не нужен, а вот 'session.name' можно определить вместо стандартного - 'PHPSESSID'. Итак дописываем в конфигурацию:
      ini_set ('session.name', 'SID');
      
    • sess_close - закрывает сессию (не разрушая сессионные переменные).
    • sess_read - читает данные из временного хранилища, в нашем случае из базы. Требует ID сессии, что из таблицы сессии надо прочитать и записать в сессию из таблицы сессий.
    • sess_write - пишет данные во временное хранилище. Требует ID сессии, и пишет все из сессии в базу.
    • sess_destroy - разрушает сессию. Требует ID сессии. Для удаления информации существует следующая функция.
    • sess_gc - это просто сборщик мусора. Требует срок хранения сессий во временном хранилище в секундах определенного в параметре 'session.gc_maxlifetime' (по умолчанию 30 минут). Определяем его, и определим время жизни сессионной куки:
      ini_set('session.gc_maxlifetime', XXX);
      ini_set('session.cookie_lifetime', YYY);
      

      'sess_gc' не всегда вызывается при инициализации сессии, есть еще одна настройка которая управляет этим параметром - 'session.gc_probability'. Этот параметр определяет вероятность запуска 'sess_gc' в процентах, соответственно валидные значения 1-100. Значение по умолчанию 1%. Т.е. это означает, что с вероятностью в 1%, при открытии новой странице сайта, будет происходить очистка сессионной таблицы, по моему опыту оптимально значение 5-10. Добавляем к конфигурации:

      ini_set ('session.gc_probability', 5);
      

Структура таблиц:

CREATE TABLE "session" (
    session_id character varying(32) NOT NULL,
    session_user_id integer DEFAULT 0 NOT NULL,
    session_counter integer DEFAULT 0 NOT NULL,
    session_ip character varying(16),
    session_agent character varying(255),
    session_last integer DEFAULT 0 NOT NULL,
    session_created integer DEFAULT 0 NOT NULL,
    session_data text
);

CREATE TABLE "user" (
    user_id character varying(32) NOT NULL,
    user_ip character varying(16),
    user_agent character varying(255),
    /* могут быть и другие поля */
);

session.php - Хранение данных сессии в MySQL таблице и функции работы с сессиями на PHP.

Используется глобальный массив $user[] с полями из таблиц БД session, user.
Подразумевается что соединение с MySQL уже установлено и определено в глобальной переменной $db.
Текущая информация сохраняется в глобальной переменной $session.

<?
$SERVER_NAME=$_SERVER['HTTP_HOST'];
$SERVER_NAME=preg_replace('/^http:\/\//', '', $SERVER_NAME);
$SERVER_NAME=preg_replace('/^www\./', '', $SERVER_NAME);
define("CookiePath","/");
define("CookieDomain",$SERVER_NAME);    //".".$SERVER_NAME    домен
define("live_sess_time","1000");

ini_set('session.auto_start', '0'); // автостарт сессий не нужен

ini_set('session.use_cookies', '1');// передавать идентификатор сессии в куках

ini_set('session.use_trans_sid', '0'); // не передавать идентификатор сессии добавляя его к URL и формам

ini_set('session.save_handler', 'user');

ini_set('session.name', 'SID'); // Имя сессии

ini_set('session.gc_maxlifetime', '1800'); // время жизни сессии, 30 минут (60*30)

//ini_set ('session.cookie_lifetime', '2000'); // 0 - кука умирает при закрытии браузера

// Задаем параметры сессионной куки: (время жизни= 0 - умрет при закрытии браузера, путь, домен, true= доступно только из https зоны)
session_set_cookie_params (0, CookiePath, CookieDomain, false);

//Выставляем вероятность запуска функции sess_gc в процентах (допустимые значения 1-100, по умолчанию равно 1%)
ini_set('session.gc_probability', 10);

function sess_open ($save_path, $session_name) {return true;}

function sess_close () {return true;}

function sess_read ($session_id) {
    global $db, $user, $session;
    if (strlen ($session_id) != 32) {
        error_log ("sess_read(): Invalid SessionID = ".$session_id);
        return '';
    }
    $sql = "SELECT `session_id`, `session_user_id`, `session_counter`, `session_ip`, `session_agent`, `session_data`
        FROM `session`
        WHERE `session_id` = '".$db->sql_escape($session_id)."' AND `session_last` > '".(time() - live_sess_time)."'";
    $result = $db->sql_query ($sql);
    if ($db->sql_numrows ($result) == 1) {
        $session = $db->sql_fetchrow ($result);
        if ($session AND $session['session_ip'] == $user['user_ip'] AND $session['session_agent'] == $user['user_agent']) {
            // выборка информации о пользователе. TODO замените при необходимости на свою !!!
            $sql = "SELECT * FROM `user`
                WHERE `user_id` = '".$db->sql_escape($session['session_user_id'])."' LIMIT 1";
            $result = $db->sql_query ($sql);
            if(!$result) {
                $result = $db->sql_error ($result);
                error_log ('sess_read(): Failed to read user info - '.$result['message']);
                return '';
            }
            else {
                $user_data = $db->sql_fetchrow ($result);
                $user = array_merge ($user, $user_data, $session); // слить три массива в один
                unset($user['session_data']);
                return $session['session_data'];
            }
        } else {
            if (isset($_REQUEST[session_name()])) sess_destroy($_REQUEST[session_name()]);
            return '';
        }
    } elseif (!$result) {
        $result = $db->sql_error ($result);
        error_log ('sess_read(): Failed to read sessions - '.$result['message']);
        return '';
    } else {
        $session = NULL;
        if (isset($_REQUEST[session_name()])) sess_destroy($_REQUEST[session_name()]);
        return '';
    }
}

function sess_write ($session_id, $session_data) {
   global $db, $user, $session;
   if (strlen ($session_id) != 32) {
      error_log ('sess_write(): Invalid Session ID = '.$session_id);
      return false;
   }
   if (4294967295 < strlen($session_data)) {
      error_log ('sess_write(): Session data too large. '.$session_id.'(max. 4294967295) -> '.strlen($session_data));
            if (isset($_REQUEST[session_name()])) sess_destroy($_REQUEST[session_name()]);
      return false;
   }
   if ($session AND $session['session_ip'] != $user['user_ip']){
            if (isset($_REQUEST[session_name()])) sess_destroy($_REQUEST[session_name()]);
      return false;
   }
   if ($session) {
      $sql = "UPDATE `session`
        SET `session_user_id` = '".intval ($session['session_user_id'])."',
            `session_last` = '".time ()."',
            `session_counter` = '".intval(++$session['session_counter'])."',
            `session_data` = '".$db->sql_escape($session_data)."'
        WHERE `session_id` = '".$db->sql_escape($session_id)."' LIMIT 1";
   } else {
      $sql = "INSERT INTO `session` (`session_id`, `session_created`, `session_last`,
                    `session_ip`, `session_agent`, `session_data`)
        VALUES ('".$db->sql_escape ($session_id)."', ".time().", ".time().",
            '".$db->sql_escape ($user['user_ip'])."',
            '".$db->sql_escape ($user['user_agent'])."',
            '".$db->sql_escape ($session_data)."')";
   }
   $result = $db->sql_query ($sql);
   if (!$result) {
      $result = $db->sql_error ($result);
      error_log ('sess_write(): Failed to INSERT/UPDATE session. '.$result['message']."<br> Query: ".$sql);
      return false;
   }
   return true;
}

function sess_destroy ($session_id) {
   global $db;
   $sql = "DELETE FROM `session`
        WHERE `session_id` = '".$db->sql_escape ($session_id)."'";
   $result = $db->sql_query ($sql);
   if (!$result) {
      $result = $db->sql_error ($result);
      error_log ('sess_destory(): Failed to DELETE session. '.$result['message']);
      return false;
   }
   return true;
}

function sess_gc ($sess_gc_maxlifetime) {
   global $db;
   $sql = "DELETE FROM `session` WHERE `session_last` < '".(time () - $sess_gc_maxlifetime)."'";
   $result = $db->sql_query ($sql);
   if (!$result) {
      $result = $db->sql_error ($result);
      error_log ('sess_gc(): Failed to DELETE old sessions.'.$result['message']);
      return false;
   }
   $sql = "OPTIMIZE TABLE `session` ";
   $result = $db->sql_query ($sql);
   if (!$result) {
      $result = $db->sql_error ($result);
      error_log ('sess_gc(): Failed to OPTIMIZE sessionstable.'.$result['message']);
      return false;
   }
   return true;
}

session_set_save_handler ("sess_open", "sess_close", "sess_read", "sess_write", "sess_destroy", "sess_gc");
// Можно активировать при проблемах
register_shutdown_function ('session_write_close');
session_start ();
?>

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

Ещё примеры работы с сесиями на PHP

Читать дальше: Безопасность в PHP


.