Как сделать авторизацию пользователя?

Авторизация пользователя на сайте – это процесс доступа к защищенному содержимому сайта, специфичному для данного пользователя. Авторизацией также можно назвать результат успешной аутентификации, когда клиентскому ПО передаются авторизационные данные, но непосредственного доступа к защищенному содержимому не происходит.

Авторизационные данные представляют собой некий обезличенный аналог аутентификационных данных (логина и пароля), который может со временем меняться вне зависимости от состояния соответствующих аутентификационных данных. Часто в качестве авторизационных данных используются числовой идентификатор пользователя и т.н. ключ авторизации, представляющий собой сложный набор символов, причем идентификатор нужен прежде всего для придания уникальности авторизационным данным. В качестве авторизационных данных можно использовать исключительно ключ авторизации, если обеспечить его уникальность. Скрипт будет передавать клиентскому ПО именно такой ключ авторизации, объединяя в нем сложный набор символов фиксированной длины (40 символов) и числовой идентификатор пользователя длиной от 1 до 10 цифр, использование которого позволит сделать ключ уникальным и ускорить процесс поиска пользователя в базе данных.

Авторизацию в виде проверки передаваемых клиентским ПО авторизационных данных нужно выполнять на каждой защищаемой странице. Аутентификацию с последующей передачей клиентскому ПО авторизационных данных можно выполнять как на каждой защищаемой странице, так и на специальной странице входа, на которую можно перенаправлять все запросы пользователя в случае неудавшейся авторизации. К преимуществам первого способа можно отнести отсутствие необходимости сохранять адрес исходной страницы, к которой осуществляется доступ, а также отсутствие необходимости иметь отдельную страницу входа. К преимуществам второго – локализацию кода аутентификации внутри одной страницы и, как следствие, упрощение кода защищаемых страниц. В скрипте будет использоваться второй способ. Отталкиваясь от него, перейти к использованию первого способа в случае возникновения такой необходимости не составит особого труда.

Прежде всего рассмотрим простейшее определение таблицы пользователей и пример записи этой таблицы:

CREATE TABLE `site_users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
  `key` varchar(40) NOT NULL,
  `login` varchar(20) NOT NULL,
  `pass` varchar(20) NOT NULL,
  `name` tinytext NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE (`login`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2;

INSERT INTO `site_users` (`id`, `key`, `login`, `pass`, `name`) VALUES
(1, '1234567890123456789012345678901234567890', 'user', '123456', 'Пользователь');

Такое простое исходное значение ключа в таблице было взято исключительно ради наглядности (нетрудно заметить, что ключ состоит из 40 символов). Скрипт будет генерировать данное значение в виде хеш-кода по алгоритму SHA1 в шестнадцатеричной системе счисления (т.е. помимо привычных арабских цифр от 0 до 9 в нем могут присутствовать и латинские буквы от a до f). Для реальных применений длину ключа и используемого в нем алфавита можно сделать еще больше (например, алфавит может состоять из 36 символов, а именно всех арабских цифр и латинских букв в одном из регистров). Также для реальных применений можно вместо исходного значения пароля хранить его хеш-код, однако следует учитывать, что в таком случае используемый пароль перестает быть уникальным для успешного доступа к учетной записи пользователя.

Код авторизации и вложенный шаблон для защищаемых страниц могут быть такими:

<?php

require PATH.'include/auth.php';

if (!($user=getuser()))
{
  header('Location: http'.(empty($_SERVER['HTTPS'])?'://':'s://').$_SERVER['HTTP_HOST'].'/login'.$_SERVER['REQUEST_URI']);
  $r0['module']='exit';
  $r0['bits']=32;
}
<p>Добро пожаловать, <strong><?= $user['name'] ?></strong>!</p>

Если планируется использовать «долгоживущие» сеансы, которые не требуют выполнения повторного входа при перезапуске браузера, необходимо после блока if добавить блок else, содержащий код пролонгирования (продления времени жизни) сеанса, полностью идентичный тому, в котором время жизни устанавливается первоначально. Смысл представленного выше кода очень прост: если авторизация не удалась, подготовиться к перенаправлению на страницу входа с добавлением к ее адресу (/login) адреса текущей страницы (содержится в $_SERVER['REQUEST_URI']). Последние две команды не должны вызывать вопросов у разработчиков, использующих G-Drive, – это подключение заголовочного файла exit.h.php, в который необходимо поместить команду exit. Содержимое включаемого файла auth.php будет рассмотрено в конце данной статьи, в том числе и функция getuser().

Код аутентификации с последующей передачей авторизационных данных и вложенный шаблон для страницы входа могут быть такими:

<?php

require PATH.'include/auth.php';

if ($_SERVER['REQUEST_METHOD']=='POST')
{
  if ($user=getuserfrompost())
  {
    setcookie('key',$user['key'].$user['id'],0,'/');
    $uri='/'.substr($_SERVER['REQUEST_URI'],6+(strlen($p)>6));
  }
  else
  {
    $uri=$_SERVER['REQUEST_URI'];
  }
  header('Location: http'.(empty($_SERVER['HTTPS'])?'://':'s://').$_SERVER['HTTP_HOST'].$uri);
  $r0['module']='exit';
  $r0['bits']=32;
}
<form method="POST">
<input type="text" maxlength="<?= AUTH_LOGLEN ?>" name="login">
<input type="password" maxlength="<?= AUTH_PASSLEN ?>" name="pass">
<input type="submit" name="submit" value="OK">
</form>

Смысл представленного выше кода также очень прост: при запросе методом POST, если аутентификация удалась, подготовиться к передаче соответствующих авторизационных данных и перенаправлению на исходную страницу, с которой когда-то могло быть выполнено перенаправление на страницу входа, иначе подготовиться к перенаправлению «на себя» (методом GET) для осуществления повторной попытки входа. Код настолько упрощен, что он не препятствует повторной попытке входа и прохождению аутентификации авторизованным пользователем.

Чтобы не создавать отдельную страницу для размещения кода принудительного выхода (завершения сеанса работы), можно добавить этот код непосредственно на страницу входа внутри имеющегося блока else:

if (!isset($_POST['login'])&&($user=getuser()))
{
  mysqli_query($link,'UPDATE `site_users` SET `key`="'.hash('sha1',uniqid()).'" WHERE `id`='.$user['id']);
  setcookie('key','',0,'/');
}

При запросе методом POST не с формы входа (отсутствует поле login), если пользователь авторизован, выполняется принудительный выход (генерация и сохранение в базе данных нового ключа авторизации на стороне сервера и удаление старого ключа авторизации на стороне клиента). Генерация нового ключа (без его передачи) будет происходить только при явном выполнении выхода пользователем. При входе клиентскому ПО будет передаваться текущий ключ авторизации, т.е. по сути он будет являться многосеансовым. Такой ключ позволяет одновременно работать сразу на нескольких устройствах. Для принудительного выхода на всех устройствах пользователю достаточно явно выполнить выход на одном из них.

Форму для выполнения принудительного выхода целесообразно разместить на каждой защищаемой странице. Например, к первому представленному в данной статье шаблону с приветствием авторизованного пользователя можно добавить такой фрагмент:

<form method="POST" action="/login<?= $_SERVER['REQUEST_URI'] ?>">
<input type="submit" name="submit" value="Выход">
</form>

Остается рассмотреть содержимое включаемого файла auth.php и ознакомиться с демонстрацией.

<?php

define('AUTH_IDLEN',10);
define('AUTH_KEYLEN',40);
define('AUTH_LOGLEN',20);
define('AUTH_PASSLEN',20);

function getuser()
{
  global $link;
  if (isset($_COOKIE['key'])&&preg_match('#^[0-9a-f]{'.AUTH_KEYLEN.'}[1-9]\\d{0,'.(AUTH_IDLEN-1).'}$#',$_COOKIE['key'])&&($res=mysqli_query($link,'SELECT * FROM `site_users` WHERE `id`='.substr($_COOKIE['key'],AUTH_KEYLEN).' AND `key`="'.substr($_COOKIE['key'],0,AUTH_KEYLEN).'"')))
  {
    $user=mysqli_fetch_assoc($res);
    mysqli_free_result($res);
    return $user;
  }
  return FALSE;
}

function getuserfrompost()
{
  global $link;
  if (isset($_POST['login'],$_POST['pass'])&&preg_match('#^[0-9a-z]{3,'.AUTH_LOGLEN.'}$#',$_POST['login'])&&preg_match('#^[0-9a-z]{6,'.AUTH_PASSLEN.'}$#',$_POST['pass'])&&($res=mysqli_query($link,'SELECT * FROM `site_users` WHERE `login`="'.$_POST['login'].'" AND `pass`="'.$_POST['pass'].'"')))
  {
    $user=mysqli_fetch_assoc($res);
    mysqli_free_result($res);
    return $user;
  }
  return FALSE;
}

Валидацию данных достаточно выполнять только при их добавлении в таблицу, а в представленном выше коде можно ограничиться экранированием данных. Кроме того, чтобы не экранировать ключ и пароль, можно вынести сравнение этих данных из условий запросов.

Демонстрация: /my/profile?p=1 (в адресной строке вместо profile можно вписать любые допустимые для пути символы, а вместо 1 – любое натуральное число до 999 включительно; данные для заполнения формы входа можно найти в статье). Внимание! При работе с демонстрацией может происходить потеря доступа к защищенному содержимому даже без перезапуска браузера или выполнения принудительного выхода. Это происходит, когда принудительный выход делает другой пользователь, работающий в данный момент с демонстрацией.

Комментарии: 12

  1. Дарья

    Хорошая статья, а можно исходники посмотреть?

  2. Михаил

    Все написанные мной исходники опубликованы в статье. Остальное – это каркас приложения для подключения обработчиков /my* и /login*. Вы можете разместить код в двух отдельных файлах в корне сайта с именами соответственно my.php и login.php, добавив ко всем ссылкам в коде расширение .php и изменив в команде $uri='/'... число 6 на число 10 (это длина префикса /login). Также основной код обоих обработчиков нужно обрамить кодом открытия соединения с БД/закрытия соединения, а код вложенных шаблонов – каким-нибудь общим шаблоном страницы, например на демонстрационном сайте, ссылка на который присутствует в статье, используется общий шаблон, практически идентичный показанному ниже (убраны только не существенные для данной статьи вставки):

    <!DOCTYPE html>
    <html lang="ru">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><?= $page['name'] ?> | Site</title>
    </head>
    <body>
    <?= $page['content'] ?>
    
    </body>
    </html>
    

    Две команды перед последней закрывающей фигурной скобкой можете заменить командой exit.

    Исходники: mods.zip

  3. Михаил

    Возле «шестерок» заметил еще использование переменной $p – в ней должен находиться путь из адреса. Пример того, как можно получить значение этой переменной, есть в статье «Как сделать единую точку входа с ЧПУ?». Можете использовать и какой-нибудь более простой способ, например:

    list($p)=explode('?',$_SERVER['REQUEST_URI'],2);
    
  4. Дарья

    Спасибо!

  5. Игорь

    Добрый день. Видел, вы писали на форумах, как сделать ЛК для пользователя. У вас получилось сделать ЛК или нет? А то задался таким же вопросом, только сам 0((

  6. Михаил

    Судя по времени сервера, у нас тогда была ночь :)

    Посмотрите демо в конце статьи. Там можно произвольно менять конец пути. Я это сделал специально, чтобы показать, что ЛК может быть представлен не одной страницей, а целой группой страниц. Естественно, в реальных условиях такую вальяжность в адресах лучше не допускать. Например, в G-Drive можно использовать не режим разрешений 1, который я использовал для демо к данной статье, а режим разрешений 2 или 3 (см. описание). В любом из этих двух режимов можно создать в разделе «Личный кабинет» (my) страницы с нужными слагами, например с пустым слагом (для режима 3) и со слагом profile, после чего использовать эти слаги в именах файлов, содержащих код реализации требуемого функционала при обращениях соответственно по адресам /my и /my/profile:

    $r0['module']='lk'.$page['id'];
    include PATH.$r0['module'].'.php';
    

    Файлы должны называться соответственно lk.php, lkprofile.php и т.п. Они должны находиться в закрытом от прямого доступа каталоге, либо в начало каждого из них нужно вставить код защиты от прямого доступа. Состав и назначение страниц ЛК зависят исключительно от ваших потребностей.

  7. Админ

    Чтобы авторизация имела хоть какое-то ограничение, препятствующее свободному подбору ключа (для конкретного значения id или в общем), можно использовать ограничение по времени, связанное с активностью пользователя на сайте. Для этого в таблицу пользователей нужно добавить дополнительное поле expire, содержащее метку времени, начиная с которой доступ по ключу становится невозможен без повторной успешной аутентификации, позволяющей обновить значение данного поля. Естественно, обновление значения поля должно происходить и во время активности пользователя на сайте, но не при каждом его обращении, чтобы такая активность не приводила к частому обновлению его записи в таблице пользователей. Для реализации этого ограничения добавьте в функцию getuser дополнительное условие, в котором бы проверялось, что текущее время не достигло значения expire. Например, можно добавить к запросу дополнительное условие AND NOW()<`expire` или AND UNIX_TIMESTAMP()<`expire` в зависимости от того, в каком формате хранится значение expire. Для обновления поля добавьте в обе показанные в статье функции перед командой return $user; следующий код:

    if ($user && $user['now']+UPDATE_PERIOD >= $user['expire'])
    {
      update($user['id'], 'expire', $user['now']+EXPIRE_PERIOD);
    }
    

    Команда update абстрактная. Ее фактическая реализация должна представлять собой код обновления поля expire значением выражения $user['now']+EXPIRE_PERIOD. В переменной $user['now'] содержится текущее время, полученное при выполнении основного запроса от сервера баз данных. Вместо нее можно использовать функцию time и т.п. Константа EXPIRE_PERIOD определяет максимально возможное время сеанса без обновления. Константа UPDATE_PERIOD определяет время до наступления expire, в течение которого необходимо обновить поле expire, чтобы продлить время сеанса. Этот промежуток времени иногда называют «красной зоной». Длительность «красной зоны» должна быть меньше EXPIRE_PERIOD, чтобы перед ней оставалась «зеленая зона», в течение которой не нужно обновлять поле expire.

  8. Роман

    Всем доброго времени!

    Насколько я понял, файлы в исходниках с "d" и есть обработчики, а как их запускать?

    Нужно скрипты писать дополнительно?

    А можете для новичка расписать?

  9. Михаил

    В комментарии со ссылкой на исходники и в следующем за ним написано, что нужно сделать, чтобы запустить исходники. Например, можете для файлов с именами my.php и login.php использовать такую структуру:

    <?php $link=mysqli_connect() or die('Error!');
    
    // здесь размещается код из d-файла или команда для вставки этого файла
    
    include 'header.php'; ?>
    
    <!-- здесь размещается код шаблона -->
    
    <?php include 'footer.php';
    

    Вообще же исходники рассчитаны на их использование совместно с G-Drive. В этом случае они могут использоваться без изменений. Достаточно разместить файлы в каталоге mods (в зависимости от настроек движка d-файлы и файлы шаблонов могут находиться в разных каталогах, d-файлы могут не иметь суффикса «.d» в именах и т.п.) и выполнить запрос(ы) для активизации соответствующих модулей, например:

    INSERT INTO `site_categories` (`id`, `name`, `module`, `bits`) VALUES
    ('login', 'Вход в личный кабинет', '', 61),
    ('my', 'Личный кабинет', '', 61)
    
  10. Михаил

    Статья обновлена. Выход по GET заменен на выход по POST.

    Спасибо за справедливую критику!

Отправить комментарий

Ваш адрес E-mail не будет опубликован.