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

Что же, давно просили более расширенный вариант авторизации. Данная авторизация использует классы. Опять оговорюсь что это примерная реализация а не оптимальная. Можно улучшить защиту, добавить разделение прав и много чего еще. Я уже не говорю о таких банальных вещах как рефакторинг. Тем не менее она вполне сносно написана во время очередного 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Copyright © Programmer Weekdays | Powered by WordPress