Сессии в 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 теперь доступна на всех страницах данного сайта, которые запустили сессии.
Другие полезные функции и приемы для работы с сессиями:
- 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>
Использование MySQL для хранения данных сессий
Для высоконагруженных проектов использование файлов для хранения файлов сессий становится недопустимым.
Сейчас мы рассмотрим использвоание БД MySql для хранения данных сессий.
- все настройки сессий производятся до старта сессии, поэтому необходимо отменить автостарт сессий:
ini_set('session.auto_start', '0');
- стандартно PHP хранит сессии в файлах, чтобы установить свои обработчики сессий определим:
ini_set('session.save_handler', 'user');
у 'session.save_handler' может быть три значения:- files - значение по умолчанию, PHP использует стандартные функции обработки сессий, сессии храняться в файлах, необходимо определить ini_set('session.save_path', путь); место для хранения файлов сессий.;
- mm - PHP использует стандартные функции обработки сессий, сессии храняться в памяти;
- user - позволяет переопределять стандартные функции обработки сессий, и соответственно в этих функциях указывать, где мы будем хранить сессии и как мы будем их обрабатывать.
- теперь определим функции обработки сессий:
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);
- sess_open - открывает сессию. Функция создает уникальное ID сессии. Требует для своей работы два
параметра 'session.save_path' и 'session.name'.
Т.к. мы храним сессии в базе, то 'session.save_path' нам не нужен, а вот 'session.name' можно определить
вместо стандартного - 'PHPSESSID'. Итак дописываем в конфигурацию:
Структура таблиц:
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' вы можете подобрать тот балланс который подходит Вашему сайту. Таким образом регулировать размер таблицы сессий.