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

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

Авторизационные данные представляют собой некий обезличенный аналог аутентификационных данных (логина и пароля), который может со временем меняться вне зависимости от состояния соответствующих аутентификационных данных. Часто в качестве авторизационных данных используются числовой идентификатор пользователя и т.н. ключ авторизации, представляющий собой сложный набор символов, причем идентификатор нужен прежде всего для придания уникальности авторизационным данным. В качестве авторизационных данных можно использовать исключительно ключ авторизации, если обеспечить его уникальность. Скрипт будет передавать клиентскому ПО именно такой ключ авторизации, объединяя в нем сложный набор символов фиксированной длины (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) для осуществления повторной попытки входа. Код настолько упрощен, что он не препятствует повторной попытке входа и прохождению аутентификации авторизованным пользователем. Чтобы улучшить поведение данной страницы, а также чтобы не создавать отдельную страницу для размещения кода принудительного выхода (завершения сеанса работы), необходимо добавить код принудительного выхода прямо на страницу входа после основного блока if внутри блока elseif:

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

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

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

<p><a href="/login<?= $_SERVER['REQUEST_URI'] ?>">Выход</a></p>

Остается рассмотреть содержимое включаемого файла 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'])&&isset($_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 включительно; данные для заполнения формы входа можно найти в статье). Внимание! При работе с демонстрацией может происходить потеря доступа к защищенному содержимому даже без перезапуска браузера или выполнения принудительного выхода. Это происходит, когда принудительный выход делает другой пользователь, работающий в данный момент с демонстрацией.

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

  1. Дарья

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

  2. Михаил

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

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv=Content-Type content="text/html; charset=utf-8">
    <meta http-equiv=Content-Language content="ru">
    <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 и т.п. Они должны находиться в закрытом от прямого доступа каталоге, либо в начало каждого из них нужно вставить код защиты от прямого доступа. Состав и назначение страниц ЛК зависят исключительно от ваших потребностей.

Комментарии, ожидающие обработки: 1

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

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