>>>

Проектирование, разработка и оптимизация веб-приложений

Сессии в PHP

С самого начала PHP все приняли на ура, но как только на этом языке стали создавать достаточно крупные проекты, разработчики столкнулись с новой проблемой - в PHP отсутствовало понятие глобальных переменных! То есть, выполнялся некий скрипт, посылал сгенерированную страницу клиенту, и все ресурсы, используемые этим скриптом уничтожались. Попробую проиллюстрировать: предположим есть две страницы одного сайта, index.php и dothings.php. Исходники к этим страницам выглядят так:

index.php

<?php
$a = "Меня задали на index.php";
?>
<html><body>
<?php
echo $a;
?>
</body></html>

dothings.php

<html><body>
<?php
echo $a;
?>
</body></html>

Если выполнить эти два скрипта, то на первой странице мы увидим надпись "Меня задали на index.php", а вторая страница будет пустой.

Разработчики web-сайтов, недолго думая, стали использовать cookie для хранения глобальных переменных на стороне клиента. Процесс выглядел примерно так: пользователь приходит на главную страницу сайта, делает какие-то действия, и вся информация, связанная с этим пользователем, которая может потребоваться на других страницах сайта, будет храниться у него в браузере в виде cookie. Этот метод имеет довольно серьезные минусы, из-за которых от PHP в своё время отвернулось немало разработчиков. Например, нам нужно авторизовать пользователя, чтобы разрешить ему доступ к закрытым (или принадлежащим только ему) разделам сайта. Придется отправлять пользователю cookie, который будет служит его последующим идентификатором на сайте. Такой подход становится очень громоздким и не удобным, как только сайт начинает собирать всё больше и больше сведений о поведении пользователя, ведь всю информацию, посылаемую пользователю, желательно кодировать, чтобы её нельзя было подделать. Ещё совсем недавно подделкой cookie можно было "уложить" не один чат, а порой и пробраться в чужую почту. К тому же есть ещё на свете странные люди, у которых браузер cookie не поддерживает.

При использовании сессий вся информация хранится не на стороне клиента, а на стороне сервера, и потому лучше защищена от манипуляций злоумышленников. Да и работать с сессиями куда проще и удобнее, так как все данные автоматически проходят через алгоритмы криптографии модуля PHP. В броузере клиента, лишь хранится уникальный идентификатор номера сессии, либо в форме cookie, либо в виде переменной в адресной строке броузера, какой из двух способов использовать для передачи идентификатора сессии между страницами интерпретатор PHP выбирает сам. Это на 100% безопасно, так как идентификатор сессии уникален, и подделать его практически невозможно (об этом чуть далее, в разделе о безопасности сессий).

Как работать с сессиями?

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

"Warning: open(/var/state/php/sess_6f71d1dbb52fa88481e752af7f384db0, O_RDWR) failed: No such file or directory (2)".

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

Любой скрипт, который будет использовать переменные (данные) из сессий, должен содержать следующую строчку:

session_start();

Эта команда говорит серверу, что данная страница нуждается во всех переменных, которые связаны с данным пользователем (браузером). Сервер берёт эти переменные из файла и делает их доступными. Очень важно открыть сессию до того, как какие-либо данные будут посылаться пользователю; на практике это значит, что функцию session_start() желательно вызывать в самом начале страницы, например так:

session_start();
?>
<html>
 <head>
 </head>
 ...

Для задания директории в которой будут сохраняться файлы сессий используется функция session_save_path():

session_save_path($_SERVER['DOCUMENT_ROOT'].'/session');
session_start();

После начала сессии можно задавать глобальные переменные. Ари присвоении какого-либо значения любому полю массива $_SESSION, переменная с таким же именем автоматически регистрируется, как переменная сессии. Этот массив доступен на всех страницах, использующих сессию. Для примера разберем програму:

index.php

<?php
 // открываем сессию
 session_start();
 // задаём значение переменной
 $_SESSION['a'] = "Меня задали на index.php";
 ?>
 <html>
 <body>
 Всё ОК. Сессию загрузили!
 Пройдём, посмотрим что <a href="dothings.php">там:</a>
 </body>
</html>

dothings.php

<?php
 // открываем сессию
 session_start();
 ?>
<html>
 <body>
 <?php
 echo $_SESSION['a'];
 ?>
 </body>
</html>

При последовательном запуске этих файлов, первый скрипт "index.php" выдаст следующий результат:

Всё ОК. Сессию загрузили! Пройдём, посмотрим что там:

А второй "dothings.php" вот это:

Меня задали на index.php

Переменная $a теперь доступна на всех страницах данного сайта, которые запустили сессии.

Другие полезные функции и приемы для работы с сессиями:

Примеры

Теперь обратимся к практическому применению механизма сессий. Здесь мы рассмотрим пару довольно простых и в то же время полезных примеров.

Авторизация Пользователя

Вопросы по авторизации пользователей с помощью PHP-сессий постоянно задаются в конференциях по web-программированию. Механизм авторизации пользователей в системе с помощью сессий довольно хорош с точки зрения безопасности (см.раздел Безопасность).

Наш пример будет состоять из трёх файлов: index.php, authorize.php и secretplace.php. Файл index.php содержит форму, где пользователь введёт свой логин и пароль. Эта форма передаст данные файлу authorize.php, который в случае успешной авторизации допустит пользователя к файлу secretplace.php, а в противном случае выдаст сообщение об ошибке.

Примеры: index.php

<html>
 <head>
     <title>Вводи пароль</title>
 </head>
 <body>
 <form action="authorize.php" method="post">
     Логин: <input type="text" name="user_name"><br>
     Пароль: <input type="password" name="user_pass"><br>
     <input type="submit" name="Submit">
 </form>
 </body>
 </html>

authorize.php

<?php
 // открываем сессию
 session_start();
 // данные были отправлены формой?
 if($_POST['Submit']){
     // проверяем данные на правильность... в данном случае я
     // вписал имя пользователя и пароль прямо в код, целесообразней
     // было бы проверить логин/пароль в базе данных и при сов-
     // падении дать доступ пользователю...
     if(($_POST['user_name']=="cleo")&&($_POST['user_pass']=="password")){
         // запоминаем имя пользователя
         $_SESSION['logged_user'] = $_POST['user_name'];
        // и переправляем его на <секретную> страницу...
         header("Location: secretplace.php");
         exit;
     }
 }
 // если что-то было не так, то пользователь получит
 // сообщение об ошибке.
 ?>
 <html><body>
 Вы ввели неверный пароль!
 </body></html>

secretplace.php

<?php
 // открываем сессию
 session_start();
 /*
   просто зайти на эту страницу нельзя... если
   имя пользователя не зарегистрировано, то
   перенаправляем его на страницу index.php
   для ввода логина и пароля... тут на самом деле
   можно много чего сделать, например запомнить
   IP пользователя, и после третьей попытки получить
   доступ к файлам, его закрыть.
 */
 if(!isset($_SESSION['logged_user'])){
     header("Location: index.php");
     exit;
 }
 ?>
 <html>
 <body>
 Привет, <?php echo $_SESSION['logged_user']; ?>, ты на секретной странице!!! :)
 </body>
 </html>

Безопасность

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

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

Впрочем, PHP очень часто можно "обмануть". Давайте рассмотрим возможные точки взлома в программе авторизации пользователя:

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

Как "залатать" дыру номер 1?

Не будем писать тонны кода по блокировке IP-адреса и т.п., а просто проверим, откуда приходит запрос, а точнее с какой страницы пришёл запрос, если это будет любая страница с нашего сайта, то всё нормально, а во всех остальных случаях пускать не будем. Подкорректируем файл authorize.php:

authorize.php V2

<?php
 // открываем сессию
 session_start();

 // полный путь к корневой директории где расположены скрипты
 $SERVER_ROOT = "http://localhost/test1/";

 // если пользователь пришёл с любой страницы нашего сайта
 // то он вроде наш...
 // Переменная $HTTP_REFERER всегда доступна по умолчанию
 // и содержит полный адрес ссылающейся страницы...
 // функция eregi() проверяет, начинается ли адрес ссылающейся страницы
 // со значения в переменной $SERVER_ROOT

 if(preg_match("/^$SERVER_ROOT/",$_SERVER['HTTP_REFERER'])){
     // данные были отправлены формой?
     if($_POST['Submit']){
         // далее все как раньше
         if(($_POST['user_name']=="cleo")&&($_POST['user_pass']=="password")){
             // запоминаем имя пользователя
             $_SESSION['logged_user'] = $_POST['user_name'];
             // и переправляем его на <секретную> страницу...
             header("Location: secretplace.php");
             exit;
         }
     }
 }
 ?>
 <html><body>
 Вы ввели неверный пароль!
 </body></html>

Как избавиться от "дыры" номер 2?

Предположим, у вас есть сайт, где каждый смертный может зарегистрироваться чтобы добавлять сообщения в форум. Естественно, в форуме у некоторых пользователей (админов, модераторов), возможностей больше чем у других, они, например, могут удалять сообщения других пользователей. Уровень доступа пользователя вы храните в сессии, в переменной $user_status, где $user_status = 10 соответствует полному доступу к системе. Пришедшему на сайт злоумышленнику достаточно зарегистрироваться штатным образом, а потом дописать в адресной строке браузера ?user_status=10. Вот и завёлся у вас на форуме новый админ!

В принципе, любую переменную скрипта можно задать через адресную строку, просто дописав после полного адреса к скрипту вопросительный знак и название переменной с её значением. Давайте поправим наш код, чтобы этого избежать:

secretplace.php V2

<?php
 // убираем всё лишнее из адресной строки
 // функция unset() <освобождает> переменную
 unset($_SESSION['logged_user']);

 // открываем сессию
 session_start();

 /*
   просто зайти на эту страницу нельзя... если
   имя пользователя не зарегистрировано, то
   перенаправляем его на страницу index.php
   для ввода логина и пароля... тут на самом деле
   можно много чего сделать, например запомнить
   IP пользователя, и после третьей попытки получить
   доступ к файлам, его перекрыть.
 */
 if(!isset($_SESSION['logged_user'])){
     header("Location: index.php");
     exit;
 }
 ?>
 <html>
 <body>
 Привет, <?php echo $_SESSION['logged_user']; ?>, ты на секретной странице!
 </body>
 </html>

Итоги

Механизм сессий - довольно удачная особенность языка PHP. Сессии просты, очень гибки в использовании. Кстати, есть одна, мало где документированная возможность сессий PHP (доступна начиная с версии 4.0.3) - в сессиях можно хранить не только переменные, но и объекты.

Примеры

<?php
// Автоматическая вставка SID в форму.
ini_set("session.use_trans_sid", true);
session_start();
?>
<form method="post">
</form>
?>
// Автоматическая вставка SID в ссылки.
ini_set("session.use_trans_sid", true);
session_start();
?>
<body>
<a href="/path/to/something.html">Click here!</a><br>
<a href="/path/to/something.php?param=value">Click here!</a><br>
<a href="http://htmlweb.ru/">Click here!</a><br>
</body>
// Пример работы с сессиями.
session_start();
// Если на сайт только-только зашли, обнуляем счетчик.
if (!isset($_SESSION['count'])) $_SESSION['count'] = 0;
// Увеличиваем счетчик в сессии.
$_SESSION['count'] = $_SESSION['count'] + 1;
?>
<h2>Счетчик</h2>
В текущей сессии работы с браузером Вы открыли эту страницу
<?=$_SESSION['count']?> раз(а).<br>
Закройте браузер, чтобы обнулить счетчик.<br>
<a href="<?=$_SERVER['SCRIPT_NAME']?>" target="_blank">
    Открыть дочернее окно браузера</a>.
// Простой пример использования сессий без Cookies.
session_name("test");
session_start();
$_SESSION['count'] = @$_SESSION['count'] + 1;
?>
<h2>Счетчик</h2>
В текущей сессии работы с браузером Вы открыли эту страницу
<?=$_SESSION['count']?> раз(а). <br>
Закройте браузер, чтобы обнулить этот счетчик.<br>
<a href="<?=$_SERVER['SCRIPT_NAME']?>?<?=SID?>">Нажмите сюда для обновления страницы!</a>

Использование 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' вы можете подобрать тот балланс который подходит Вашему сайту. Таким образом регулировать размер таблицы сессий.


Размер шрифта:
А
А
А
Цвет сайта:
A
A
A
Изображение:
Вкл.
Выкл.
Обычная версия