Что же, давно просили более расширенный вариант авторизации. Данная авторизация использует классы. Опять оговорюсь что это примерная реализация а не оптимальная. Можно улучшить защиту, добавить разделение прав и много чего еще. Я уже не говорю о таких банальных вещах как рефакторинг. Тем не менее она вполне сносно написана во время очередного night programming.
Пожалуй стоит начать с описании логики.
Как известно время хранения сессии 30 минут. Железно увеличивать ее в настройках сервера не стоит, это повысит нагрузку на сервер и скорее всего приведет к потери производительности. Так же мы можем назначить свою папку для хранения сессий, но тогда нам нужно писать инструмент для ее очистки — иначе мы будем хранить все сессии которые когда либо существовали, а нам этого не нужно. Это опять таки увеличит нагрузку, снизит время отклика, а если вы используете маленький хостинг план то у вас вообще место может закончится, при условии что очистка этой папки была написана с ошибками.
Оптимальный вариант это комбинировать сессии с куками. В первую очередь при обращении к странице проверяется существование сессии. Если сессии нету то мы проверяем наличие куки.
Соответственно если кук нету мы отправляем человека на форму авторизации. Если же они есть мы сравниваем их с записью которая есть в нашей базе. Тут уже много вариантов для увеличения безопасности. Будь то случайно сгенерированный код, user agent, ip пользователя и черт знает еще что. Оговорюсь что проверку на ip в данное время делать не стоит. Дело в том что сейчас очень распространены мобильные устройства, мощные смартфоны и планшеты… Все чаще появляется мобильный трафик.
При чем тут это? Все просто. При использовании мобильного интернета вы не имеете белого ip (я уже не говорю о статическом ip) и каждый раз когда вы заново подключаетесь к сети меняется сервер через который идет ваш трафик (соответственно и ip) да и ваш ip тоже меняется.
Думаю что пора приступать к коду.
Для начала создадим две таблички в MySQL:
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 — наш файл конфигурации.
<?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
<?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 который отвечает за регистрацию пользователей.
<?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 который отвечает за восстановление пароля
<?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
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 использует три параметра, сам запрос, его тип и количество.
Для простоты можно сделать их необязательными, например так:
function query($query, $type=null, $num=null)
Тогда по умолчанию будет возвращаться дескриптор результата запроса (resource, подробно на php.net).
Далее опишу методы класса mysql
###
# Проверка входных данных при регистрации
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';
}
Комментарий в начале и название говорят сами за себя. Здесь мы просто проверяем данные формируя массив ошибок.
###
# Регистрация
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() когда можно обойтись одним.
###
# Проверка авторизации
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;
}
}
Метод отвечающий за проверку авторизации. Для начала проверяем наличие сессии. Если сессии отсутствуют то мы проверяем куки. Экранируем их содержимое и сверяем их с таблицей сессий. Если все данные совпадают то мы стартуем сессию. Обновляем куки.
###
# Авторизация
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;
}
}
Данный метод отвечает за авторизацию. Мы экранируем введенные данные и проверяем наличие такого пользователя в базе. При успешном поиске мы запускаем сессию, обновляем данные в таблице сессий, обновляем куки. Иначе сообщаем об полученной ошибке.
###
# Выход
function exit_user() {
//~ разрушаем сессию, удаляем куки и отправляем на главную
session_destroy();
setcookie("id_user", '', time()-3600);
setcookie("code_user", '', time()-3600);
header("Location: index.php");
}
Тут все просто, удаляем сессию, удаляем куки и перенаправляем на страницу авторизации.
###
# Восстановление пароля
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 (он должен соответствовать указанному при регистрации). Генерируем новый пароль отправляем его на почту и заносим в базу.
###
# Функция генерации случайной строки
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.
Добавить комментарий