Авторизация на сессиях и куках, PHP+MySQL v.2

Что же, давно просили более расширенный вариант авторизации. Данная авторизация использует классы. Опять оговорюсь что это примерная реализация а не оптимальная. Можно улучшить защиту, добавить разделение прав и много чего еще. Я уже не говорю о таких банальных вещах как рефакторинг. Тем не менее она вполне сносно написана во время очередного night programming.
Пожалуй стоит начать с описании логики.
Как известно время хранения сессии 30 минут. Железно увеличивать ее в настройках сервера не стоит, это повысит нагрузку на сервер и скорее всего приведет к потери производительности. Так же мы можем назначить свою папку для хранения сессий, но тогда нам нужно писать инструмент для ее очистки — иначе мы будем хранить все сессии которые когда либо существовали, а нам этого не нужно. Это опять таки увеличит нагрузку, снизит время отклика, а если вы используете маленький хостинг план то у вас вообще место может закончится, при условии что очистка этой папки была написана с ошибками.
Оптимальный вариант это комбинировать сессии с куками. В первую очередь при обращении к странице проверяется существование сессии. Если сессии нету то мы проверяем наличие куки.
Соответственно если кук нету мы отправляем человека на форму авторизации. Если же они есть мы сравниваем их с записью которая есть в нашей базе. Тут уже много вариантов для увеличения безопасности. Будь то случайно сгенерированный код, user agent, ip пользователя и черт знает еще что. Оговорюсь что проверку на ip в данное время делать не стоит. Дело в том что сейчас очень распространены мобильные устройства, мощные смартфоны и планшеты… Все чаще появляется мобильный трафик.
При чем тут это? Все просто. При использовании мобильного интернета вы не имеете белого ip (я уже не говорю о статическом ip) и каждый раз когда вы заново подключаетесь к сети меняется сервер через который идет ваш трафик (соответственно и ip) да и ваш ip тоже меняется.
Думаю что пора приступать к коду.
Для начала создадим две таблички в MySQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | CREATE TABLE IF NOT EXISTS `session` ( `id_user` INT(5) NOT NULL, `code_sess` VARCHAR(15) NOT NULL, `user_agent_sess` VARCHAR(255) NOT NULL, PRIMARY KEY (`id_user`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `users` ( `id_user` INT(5) NOT NULL AUTO_INCREMENT, `login_user` VARCHAR(60) NOT NULL, `passwd_user` VARCHAR(255) NOT NULL, `mail_user` VARCHAR(255) NOT NULL, PRIMARY KEY (`id_user`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; |
Из их названий понятно что первая содержит текущие сессии а вторая данные о пользователях.
Далее естественно conf.php — наш файл конфигурации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php //~ Старт сессии, файл должен быть сохранен без DOM информации session_start(); include_once 'module.php'; //~ Параметры подключения к бд $db_host = 'localhost'; $db_login = ''; //~ логин для подключения $db_passwd = ''; //~ пароль для подключения $db_name = ''; //~ Имя таблицы // подключаемся к бд $db = new mysql(); //~ Создаем новый объект класса $db -> connect($db_host, $db_login, $db_passwd, $db_name); ?> |
Тут даже комментировать нечего. Прописываем данные для MySQL и выполняем подключение.
Далее наш index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <?php include_once 'conf.php'; ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> <title></title> </head> <body> <?php $r=''; $auth = new auth(); //~ Создаем новый объект класса //~ Авторизация if (isset($_POST['send'])) { if (!$auth->authorization()) { $error = $_SESSION['error']; unset ($_SESSION['error']); } } //~ выход if (isset($_GET['exit'])) $auth->exit_user(); //~ Проверка авторизации if ($auth->check()) $r.='Добро пожаловать '.$_SESSION['login_user'].'<br/><a href="?exit">Выйти</a>'; else { //~ если есть ошибки выводим их и предлагаем восстановить пароль if (isset($error)) $r.=$error.'<a href="recovery.php">Восстановить пароль</a><br/>'; $r.=' <a href="join.php">Зарегистрироваться</a> <form action="" method="post"> login <input type="text" name="login" value="'.@$_POST['login'].'" /><br /> passwd <input type="password" name="passwd" id="" /><br /> <input type="submit" value="send" name="send" /> </form> '; } print $r; ?> </body> </html> |
Что бы все было просто понять я не стал разделять html и php. Тут тоже все просто, вся магия будет потом ^_^.
Первое что мы делаем это подключаем наш конфиг (соответственно уже тогда у нас выполняется подключение к MySQL и старт сессий). Далее мы создаем объект класса auth и проверяем существует ли попытка авторизоваться.
Чуть ниже мы прописываем выход который принимаем через $_GET.
За проверку авторизации у нас служит метод $auth->check(), он возвращает true или false соответственно. Т.е. мы проверяем авторизации и далее уже выводим «добро пожаловать» или форму регистрации.
Переходим к файлу join.php который отвечает за регистрацию пользователей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <?php include_once 'conf.php'; ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> <title></title> </head> <body> <?php $reg = new auth(); //~ Создаем новый объект класса $form = ' <a href="join.php">Авторизоваться</a><br /> <form action="" method="post"> логин <input type="text" name="login" id="" value="'.@$_POST['login'].'" /><br /> пароль <input type="password" name="passwd" id="" /><br /> повторите пароль <input type="password" name="passwd2" id="" /><br /> Почта <input type="text" name="mail" value="'.@$_POST['mail'].'" /><br /> <input type="submit" value="send" name="send" /> </form> '; if (isset($_POST['send'])) { if ($reg->reg($_POST['login'], $_POST['passwd'], $_POST['passwd2'], $_POST['mail'])) { print ' <h2>Регистрация успешна.</h2> Вы можете войти <a href="index.php">авторизоваться</a>. '; } else print $form; } else print $form; ?> </body> </html> |
Тут все аналогично тому что было раньше. Проверяем наличие отправки формы, проверяем ответ метода $auth->reg() и предлагаем перейти к авторизации.
Далее файл recovery.php который отвечает за восстановление пароля
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php include_once 'conf.php'; ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> <title></title> </head> <body> <?php $reg = new auth(); //~ Создаем новый объект класса $r=''; $form=' <form action="" method="post"> логин <input type="text" name="login" id="" value="'.@$_POST['login'].'" /><br /> Почта <input type="text" name="mail" value="" /><br /> <input type="submit" value="send" name="send" /> </form> '; if (isset($_POST['send'])) { //~ запрос на восстановление пароля $reply = $reg->recovery_pass($_POST['login'], $_POST['mail']); if ($reply=='good') { //~ положительный ответ $r.='Новый пароль был выслан вам на почту'; } else { //~ ошибка во время восстановления $r.=$reply.$form; } } else $r.=$form; print $r; ?> </body> </html> |
Тут даже объяснять нечего.
А теперь перейдем к самому большому файлу в котором происходит вся магия — module.php.
Т.к. в нем чуть больше 200 строк то я его приведу кусками поясняя основные моменты.
Начнем с класса mysql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class mysql { ### # Подключение к бд function connect($db_host, $db_login, $db_passwd, $db_name) { mysql_connect($db_host, $db_login, $db_passwd) or die ("MySQL Error: " . mysql_error()); //~ устанавливаем подключение с бд mysql_query("set names utf8") or die ("<br>Invalid query: " . mysql_error()); //~ указываем что передаем данные в utf8 mysql_select_db($db_name) or die ("<br>Invalid query: " . mysql_error()); //~ выбираем базу данных } ### # Запрос к базе и его производные function query($query, $type, $num) { if ($q=mysql_query($query)) { switch ($type) { case 'num_row' : return mysql_num_rows($q); break; case 'result' : return mysql_result($q, $num); break; case 'accos' : return mysql_fetch_assoc($q); break; case 'none' : return $q; default: return $q; } } else { print 'Mysql error: '.mysql_error(); return false; } //~ !!! DANGER !!! //~ при переносе в паблик убрать print 'Mysql error: '.mysql_error(); //~ эта строчка стоит только для отладки и используя ее в паблике можно засветить запросы } ### # экранирование данных function screening($data) { $data = trim($data); //~ удаление пробелов из начала и конца строки return mysql_real_escape_string($data); //~ экранирование символов } } |
Этот небольшой класс был написан что бы облегчить работу с MySQL не используя при этом PDO и MySQLi (о них мы поговорим позже).
Он содержит всего три метода, подключение, запросы и экранирование. Метод mysql->query использует три параметра, сам запрос, его тип и количество.
Для простоты можно сделать их необязательными, например так:
1 | function query($query, $type=null, $num=null) |
Тогда по умолчанию будет возвращаться дескриптор результата запроса (resource, подробно на php.net).
Далее опишу методы класса mysql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | ### # Проверка входных данных при регистрации function check_new_user($login, $passwd, $passwd2, $mail) { //~ Проверка валидности данных if (empty($login) or empty($passwd) or empty($passwd2)) $error[]='Все поля обязательны для заполнения'; if ($passwd != $passwd2) $error[]='Введенные пароли не совпадают'; if (strlen($login)<3 or strlen($login)>30) $error[]='Длинна логина должна быть от 3 до 30 символов'; if (strlen($passwd)<3 or strlen($passwd)>30) $error[]='Длинна пароля должна быть от 3 до 30 символов'; //~ Валидация почты не используя регулярки http://www.php.net/manual/en/filter.examples.validation.php if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) $error[]='Не корректный email'; //~ Проверяем наличее пользователя с таким именем в бд $db = new mysql(); //~ Создаем новый объект класса $login = $db->screening($login); if ($db->query("SELECT * FROM users WHERE login_user='".$login."';", 'num_row', '')!=0) $error[]='Пользователь с таким именем уже существует'; if ($db->query("SELECT * FROM users WHERE mail_user='".$mail."';", 'num_row', '')!=0) $error[]='Пользователь с таким email уже существует'; //~ Возвращаем массив ошибок или положительный ответ if (isset($error)) return $error; else return 'good'; } |
Комментарий в начале и название говорят сами за себя. Здесь мы просто проверяем данные формируя массив ошибок.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ### # Регистрация function reg($login, $passwd, $passwd2, $mail) { if (($this->check_new_user($login, $passwd, $passwd2, $mail))=='good') { $db = new mysql(); //~ Создаем новый объект класса $passwd = md5($db->screening($passwd).'lol'); //~ хеш пароля с солью $login = $db->screening($login); if ($db->query("INSERT INTO `users` (`id_user`, `login_user`, `passwd_user`, `mail_user`) VALUES (NULL, '".$login."', '".$passwd."', '".$mail."');", '', '')) return true; else { print 'Возникла ошибка при регистрации нового пользователя. Свяжитесь с администрацией'; return false; } } else { print $this->error_print($this->check_new_user($login, $passwd, $passwd2, $mail)); return false; } } |
Сам метод регистрации. Проверяем данные на выходе предыдущим методом, делаем хеш пароля с солью и регистрируем пользователя. Кстати вот очевидный пример где нужно сделать рефакторинг — два вызова auth->check_new_user() когда можно обойтись одним.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | ### # Проверка авторизации function check() { if (isset($_SESSION['id_user']) and isset($_SESSION['login_user'])) return true; else { //~ проверяем наличие кук if (isset($_COOKIE['id_user']) and isset($_COOKIE['code_user'])) { //~ куки есть - сверяем с таблицей сессий $db = new mysql(); //~ создаем новый объект класса $id_user=$db->screening($_COOKIE['id_user']); $code_user=$db->screening($_COOKIE['code_user']); if ($db->query("SELECT * FROM `session` WHERE `id_user`=".$id_user.";", 'num_row', '')==1) { //~ Есть запись в таблице сессий, сверяем данные $data = $db->query("SELECT * FROM `session` WHERE `id_user`=".$id_user.";", 'accos', ''); if ($data['code_sess']==$code_user and $data['user_agent_sess']==$_SERVER['HTTP_USER_AGENT']) { //~ Данные верны, стартуем сессию $_SESSION['id_user']=$id_user; $_SESSION['login_user']=$db->query("SELECT login_user FROM `users` WHERE `id_user` = '".$id_user."';", 'result', 0); //~ обновляем куки setcookie("id_user", $_SESSION['id_user'], time()+3600*24*14); setcookie("code_user", $code_user, time()+3600*24*14); return true; } else return false; //~ данные в таблице сессий не совпадают с куками } else return false; //~ в таблице сессий не найден такой пользователь } else return false; } } |
Метод отвечающий за проверку авторизации. Для начала проверяем наличие сессии. Если сессии отсутствуют то мы проверяем куки. Экранируем их содержимое и сверяем их с таблицей сессий. Если все данные совпадают то мы стартуем сессию. Обновляем куки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ### # Авторизация function authorization() { $db = new mysql(); //~ создаем новый объект класса $login = $db->screening($_POST['login']); $passwd = md5($db->screening($_POST['passwd']).'lol'); //~ хеш пароля с солью if ($db->query("SELECT * FROM `users` WHERE `login_user` = '".$login."' AND `passwd_user` = '".$passwd."';", 'num_row', '')==1) { //~ пользователь найден в бд, логин совпадает с паролем $_SESSION['id_user']=$db->query("SELECT * FROM `users` WHERE `login_user` = '".$login."' AND `passwd_user` = '".$passwd."';", 'result', 0); $_SESSION['login_user']=$login; //~ добавляем/обновляем запись в таблице сессий и ставим куку $r_code = $this->generateCode(15); if ($db->query("SELECT * FROM `session` WHERE `id_user`=".$_SESSION['id_user'].";", 'num_row', '')==1) { //~ запись уже есть - обновляем $db->query("UPDATE `session` SET `code_sess` = '".$r_code."', `user_agent_sess` = '".$_SERVER['HTTP_USER_AGENT']."' WHERE `id_user` = ".$_SESSION['id_user'].";", '', ''); } else { //~ записи нету - добавляем $db->query("INSERT INTO `session` (`id_user`, `code_sess`, `user_agent_sess`) VALUES ('".$_SESSION['id_user']."', '".$r_code."', '".$_SERVER['HTTP_USER_AGENT']."');", '', ''); } //~ ставим куки на 2 недели setcookie("id_user", $_SESSION['id_user'], time()+3600*24*14); setcookie("code_user", $r_code, time()+3600*24*14); return true; } else { //~ пользователь не найден в бд, или пароль не соответствует введенному if ($db->query("SELECT * FROM `users` WHERE `login_user` = '".$login."';", 'num_row', 0)==1) $error[]='Введен не верный пароль'; else $error[]='Такой пользователь не существует'; $_SESSION['error'] = $this->error_print($error); return false; } } |
Данный метод отвечает за авторизацию. Мы экранируем введенные данные и проверяем наличие такого пользователя в базе. При успешном поиске мы запускаем сессию, обновляем данные в таблице сессий, обновляем куки. Иначе сообщаем об полученной ошибке.
1 2 3 4 5 6 7 8 9 | ### # Выход function exit_user() { //~ разрушаем сессию, удаляем куки и отправляем на главную session_destroy(); setcookie("id_user", '', time()-3600); setcookie("code_user", '', time()-3600); header("Location: index.php"); } |
Тут все просто, удаляем сессию, удаляем куки и перенаправляем на страницу авторизации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | ### # Восстановление пароля function recovery_pass($login, $mail) { $db = new mysql(); //~ создаем новый объект класса $login = $db->screening($login); $db_inf = $db->query("SELECT * FROM `users` WHERE `login_user`='".$login."';", 'accos', ''); if ($db->query("SELECT * FROM `users` WHERE `login_user`='".$login."';", 'num_row', '')!=1) { //~ не найден такой пользователь $error[]='Пользователь с таким именем не найден'; return $this->error_print($error); } else { //~ проверка email if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) $error[]='Введен не корректный email'; if ($mail != $db_inf['mail_user']) $error[]='Введенный email не соответствует введенному при регистрации '; if (!isset($error)) { //~ восстанавливаем пароль $new_passwd = $this->generateCode(8); $new_passwd_sql = md5($new_passwd.'lol'); $message = "Вы запросили восстановление пароля на сайте %sitename% для учетной записи ".$db_inf['login_user']." \nВаш новый пароль: ".$new_passwd."\n\n С уважением администрация сайта %sitename%."; if (mail($mail, "Восстановление пароля", $message, "From: webmaster@sitename.ru\r\n"."Reply-To: webmaster@sitename.ru\r\n"."X-Mailer: PHP/" . phpversion())) { //~ почта отправлена, обновляем пароль в базе $db->query("UPDATE `users` SET `passwd_user`='".$new_passwd_sql."' WHERE `id_user` = ".$db_inf['id_user'].";", '', ''); //~ все успешно - возвращаем положительный ответ return 'good'; } else { //~ ошибка при отправке письма $error[]='В данный момент восстановление пароля не возможно, свяжитесь с администрацией сайта'; return $this->error_print($error); } } else return $this->error_print($error); } } |
Метод для восстановления пароля. Экранируем введенный логин и ищем такого пользователя в базе данных. Если не найден — сообщение об ошибке. Если найден то проверяем введенный email (он должен соответствовать указанному при регистрации). Генерируем новый пароль отправляем его на почту и заносим в базу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ### # Функция генерации случайной строки function generateCode($length) { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRQSTUVWXYZ0123456789"; $code = ""; $clen = strlen($chars) - 1; while (strlen($code) < $length) { $code .= $chars[mt_rand(0,$clen)]; } return $code; } ### # Формирование списка ошибок function error_print($error) { $r='<h2>Произошли следующие ошибки:</h2>'."\n".'<ul>'; foreach($error as $key=>$value) { $r.='<li>'.$value.'</li>'; } return $r.'</ul>'; } |
Два остававшихся метода. Первый генерирует случайную строку указанной длинны а второй делает из массив html список, для более наглядного вывода ошибок.
Вот впрочем и все. Собственно как я уже говорил здесь еще много чего можно улучшить, добавить. Некоторые вещи лучше сделать вообще по другому. Но для примера более сложной авторизации чем предыдущие — более чем сойдет.
Код можно взять из моего репозитория на github.
Здравствуйте. Помогите найти решения, после установки скрипта вместо текста c восстановлением пароля, на почту приходят иероглифы, как поменять на нормальный текст? Везде стоит UTF 8. Во всех браузерах одно и тоже.
[Ответить]
ZMan Reply:
Март 5th, 2014 at 14:11
Добрый день, попробуйте «поиграть» с кодировкой в шапке письма
[Ответить]
Добрый день!
1. тоже была ошибка о не найденном классе «auth»
решил путем того что прописал: «class auth {» после 36 строки в файл module.php, а в конец файла добавил закрывающую фигурную скобку «}»
2. у меня появился другой вопрос, собственно как авторизация должна и работать по идее.
хотелось бы конечно реализовать через модальное окно, но это оставлю на будущее.
просто скажем есть страница, назовем её index2.php, и необходимо чтобы к ней имели имели доступ только зарегистрированные пользователи.
и скажем страница index3.php к ней могут иметь доступ пользователи без регистрации.
чтобы ограничить доступ мне необходимо инклудить index.php или join.php из этой темы?
3. Можете подкинуть идей как грамотнее реализовать группы пользователей и разграничение их прав? Группы пользователей предполагается хранить в mysql и запрашивать из скрипта кто к какой принадлежит.
[Ответить]
ZMan Reply:
Март 5th, 2014 at 14:10
Добрый день, по вашим вопросам:
1-2) в конце статьи есть ссылка на GitHub с исходниками. Скачайте их и посмотрите как сделано. Там тот же код но не разбитый на части. Возможно вы что то упустили из вида.
3) уже не один раз писал в комментариях:
2
3
4
5
//~ доступно для гостей
} else {
//~ не доступно для гостей
}
и страница только для авторизированных пользователей (редирект в случае провала проверки авторизации)
2
3
4
//~ совершаем процедуру выхода
$auth->exit_user();
}
[Ответить]
Спасибо за оперативные ответы в комментариях, ваша наглядная реализация более менее помогла понять как должно работать.
думаю подтяну теорию и буду пробовать делать простенький рефакторинг.
ссылку на github к сожалению упустил из вида, приму к сведению
—
С уважением Роман
[Ответить]
Ошибку исправила, в файле module.php, когда открыла Блокнотом, действительно, не было прописано название класса auth { А при открытии редактором NotePad++ название было и документ был сохраненным. В общем.. спасибо за «Авторизацию» ! 🙂
[Ответить]
в файле join.php в строчке 13-14
$form = ‘
Авторизоваться
— ссылка не на авторизацию а на регистрацию
логичнее
$form = ‘
Авторизоваться
[Ответить]