Сессии в 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% безопасно, так как идентификатор сессии уникален, и подделать его практически невозможно (об этом чуть далее, в разделе о безопасности сессий).

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

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

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

"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();
?>
?lt;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 теперь доступна на всех страницах данного сайта, которые запустили сессии.

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

  • unset($_SESSION['a']) - сессия "забывает" значение заданной сессионой переменной;
  • session_destroy() - сессия уничтожается (например, если пользователь покинул систему, нажав кнопку "выход");
  • session_set_cookie_params(int lifetime [, string path [, string domain]]) - с помощью этой функции можно установить, как долго будет "жить" сессия, задав unix_timestamp определяющий время "смерти" сессии. По умолчанию, сессия "живёт" до тех пор, пока клиент не закроет окно браузера.
  • session_write_close() - запись переменных сесии и закрытие ее. Это необходимо для открытия сайта в новом окне, если страница выполняет длительную обработу и заблокировала для вашего браузера файл сессий.

Примеры

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

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

Вопросы по авторизации пользователей с помощью 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 бит), шансов, что его удастся подобрать перебором, практически нет. Поэтому злоумышленнику остаются следующие возможности:

  • на компьютере пользователя стоит "троян", который ворует номера сессий;
  • злоумышленник отлавливает трафик между компьютером пользователя и сервером. Конечно, есть защищенный (зашифрованный) протокол SSL, но им пользуются не все;
  • к компьютеру нашего пользователя подошел сосед и стащил номер сессии.

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

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

  • Файл authorize.php - попытка подбора пароля с помощью стороннего скрипта;
  • Файл secretplace.php - попытка обмануть программу путём вписывания значений переменной $logged_user в адресной строке браузера, например так:
    "http://www.yoursite.ru/secretplace.php?logged_user=hacker"

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

Как "залатать" дыру номер 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>

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

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


.