Блог Веб-разработчика.

Несмотря на наличие большого количества различных CMS (систем управления контентом), готовых скриптов и сервисов способных реализовать что-то вроде закрытого контента, доступ к которому открывается только после ввода правильной пары логин/пароль, иногда может стоять задача сделать быстрый скрипт на коленке, работа которого максимально понятна, проста и обслуживается минимальным количеством файлов. Именно такую задачу недавно пришлось решать мне, и в этой статье хочу поделиться моей наработкой.

Общий принцип работы.

Наша задача сделать какой-то простой скрипт, который позволит, во-первых, убедиться что данный пользователь имеет право получить доступ к содержимому закрытого файлика (по паре логин-пароль, который должен (в хорошем случае) знать только один человек), и во-вторых, как-то запомнить компьютер этого человека, на случай если закрытый раздел состоит из нескольких документов, между которыми возможны переходы.

Для реализации этой задачи нам потребуется база данных MySQL, где будет храниться информация о параметрах пользователя, какой-то php скрипт, а “запоминать” пользователя будем путем передачи в браузер файла cookies. То есть таким образом мы “пометим” его компьютер.

В начале каждого документа, будем делать проверку наличия файла cookie и адекватности его содержимого. Если проверка пройдена успешно - то отдаем контент, если нет - перенаправляем на форму авторизации или просто подгружаем ее через include, заблокировав при этом содержание страницы. Вот такая общая идея. Без хитростей и сложностей.

Безопасность.

Хочу сразу оговориться: технология cookies устроена таким образом, что есть возможность похищения информации в этих файлах с компьютера пользователя (например с помощью троянов или javascript сценариев на страницах сайтов злоумышленников) с последующей передачей ее хакерам. И они могут использовать эту информацию для несанкционированного доступа. Поэтому я сразу не рекомендую вам использовать coockies для защиты закрытого контента, за котором могут охотиться (например контактные данные, номера счетов, важные документы и прочее).

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

Таблица в базе данных.

Итак, для начала любым удобным для себя образом (я например, делаю это обычно через phpMyAdmin) создайте базу данных и пользователя базы данных со всеми привелегиями, и эту информацию запомните. В этой базе данных создайте таблицу следующего вида:

то есть, таблица состоит из 5 полей:

  • id - заполняется автоматически при создании новой записи (auto_increment);
  • name - имя пользователя или логин, желательно сделать это поле уникальным, чтобы не было в базе пользователей с одинаковыми логинами;
  • password - хранит md5-хэш строки из пароля пользователя и секретного ключа;
  • secretkey - секретный ключ, который будет генерироваться при каждой авторизации;
  • last_login_datetime - метка UNIX-времени, когда была произведена последняя авторизация. На всякий случай, пригодится. Это поле переписывается каждый раз, при авторизаации.

И в этой таблице создам первого пользователя, через тот же phpMyAdmin (просто чтобы уже была информация в ней):

HTML форма авторизации.

Форму авторизации сделаем самой простой, извращаться не будем. И поместим ее в файл login.php:

    <form action="<?php echo $_SERVER["PHP_SELF"];?>" method="POST">
    <span>Логин:</span>
    <input name="name" type="text" size="45"/>
    <span>Пароль:</span>
    <input name="password" type="password" size="45"/>
    <input class="submit" type="submit" value="Войти"/>
    </form>

В этот же файл, в начало, будет помещен php-обработчик введенных в форму аторизации данных. А в параметр action формы введен php-код:

  <?php echo $_SERVER["PHP_SELF"];?>

что автоматически  создает ссылку для отправки данных в текущий документ, в котором располагается форма. Все остальное стандартно и просто: два поля “логин” и “пароль”, и кнопка “войти”. Опять же - без хитростей.

PHP-обработчик формы авторизации.

Общий план работы обработчика мне видится так:

  • получив логин и пароль введенные в форму авторизации, скрипт проверяет логин на корректность (защита от SQL-инъекции);
  • далее обращается к базе данных и ищет пользователя с таким логином;
  • если такой найден, то извлекает из базы данных секретный ключ пользователя, приписывает его к присланному из формы авторизации паролю и вычисляет md5-хэш.
  • полученный md5-хэш сравнивается с md5-хэшстрокой из поля “password” текущего пользователя, которая ранее была получена именно таким же способом при создании пользователя;
  • если строки несовпадают, то выводим сообщение об ошибке и снова выводим форму авторизации. Мол, попробуй еще раз... от брутфорса, то есть подбора пароля путем перебора - пока зщищаться не будем;
  • если строки совпали, то генерируем новый секретный ключ;
  • далее приписываем секретный ключ к присланному паролю, и из него вычисляем md5-хэш;
  • всю вычисленную информацию записываем в базу данных;
  • формируем новую строку из “secretkey” поля пользователя в базе данных, текущего ip-адреса машины пользователя и даты из поля “last_login_datetime” - и из всего этого вычисляем md5-хэш;
  • полученной строке приписываем через двоеточие логин пользователя, записываем это в cookies браузера пользователя;
  • перенаправляем на закрытый контент.

Практическая реализация такого алгоритма в процедурном виде может выглядеть так:

if (isset($_POST["name"]) and isset($_POST["password"]) and $_POST["password"] !== "" and $_POST["name"] !== "") {
  if (preg_match("/^[a-zA-Z0-9]{3,30}$/", $_POST["name"])) {
    $user = $dbconnect->query("SELECT * FROM users WHERE name="".$_POST["name"].""");
    $U = $user->num_rows;
    if ($U == 1){
      $user_data = $user->fetch_array();
      $password_hash = md5($_POST["password"].":".$user_data["secretkey"]);
      if ($password_hash == $user_data["password"]){
        $secret_key = uniqid();
        $new_password_hash = md5($_POST["password"].":".$secret_key);
        $curr_date = time();
        $user_update = $dbconnect->query("UPDATE users SET `password`="".$new_password_hash."",`secretkey`="".$secret_key."",`last_login_datetime`=".$curr_date." WHERE `name`="".$_POST["name"].""");
        if ($user_update){
          setcookie("WebEngineerRestrictedArea",$_POST["name"].":".md5($secret_key.":".$_SERVER["REMOTE_ADDR"].":".$curr_date),time() 60*60*24);
          header ("Location: ".$_SERVER["PHP_SELF"]);
          exit();
        } else {
          $update_error = TRUE;
        }
      } else {
        $error_pass = TRUE;
      }
    } else {
      $error_login = TRUE;
    }
  } else {
    $syntax_error = TRUE;
  }
}

после записи в cookies браузера соответствующих данных, отдача закрытого контента происходит путем перенаправления (header(“Location: ...”);) пользователя на тот же скрипт, куда была подгружена форма авторизации. В результате он снова проходит проверку, которая на этот раз дает положительный результат и клиент получает доступ в закрытый раздел.

Обратите внимание на то, какие данные записываются в cookie: записывается переменная (в данном случае “WebEngineerRestrictedArea”) и ей присваивается значение равное строке:

//----логин-----:--хэш(секр. ключ---:-----IP-дрес клиента-------:дата авторизации)
$_POST["name"].":".md5($secret_key.":".$_SERVER["REMOTE_ADDR"].":".$curr_date)

то есть генерация хэш-строки привязывается к IP-адресу машины клиента. Это позволит в значительной степени минимизировать риск несанкционированного доступа, через кражу информации из файлов cookies. Так как в каждом закрытом документе, мы будем проверять не только наличие данной переменной в cookies браузера клиента, но и вычислим его хэш с использованием реального IP-адреса компьютера, с которого пришел запрос. Таким образом, даже если информация из cookies была украдена, доступ все равно получить не удастся, потому что система не сможет на основе другого IP-адреса вычислить такой же хэш-код, как в cookies. А значит предложит авторизоваться.

Однако, есть исключение. Если клиент и злоумышленник выходят в интернет через общий прокси-сервер, то они в сети имеют один и тот же внешний IP-адрес - поэтому для таких случаев подобная защита может оказаться недействительной. Остается лишь вопрос вероятности такого совпадения... Но, однозначно она очень не высокая. Особенно учитывая необходимость знать структуру исходной строки, из которой вычисляется защитный md5-хэш.

То, что сегодня большинство IP-адресов динамические, это нам тоже не мешает. Как правило, адрес меняется при каждом выходе в интернет, а это означает, что в течение всей сессии работы в интернете, пользователь будет иметь возможность доступа к закрытому разделу, без необходимости повторной авторизации. Но, только при том условии, что он будет поддерживать связь с интернетом через текущий IP-адрес не более суток. Так как время жизни переданных данных в cookies определено на 24 часа с момента передачи, после чего снова потребуется авторизация.

Проверка пользователя на право доступа к закрытому контенту.

Общий план проверки пользователя, мне представляется таким образом:

  • сначала проверяем, есть ли вообще в кукисах браузера клиента наша переменная (в моем случае “WebEngineerRestrictedArea”);
  • если ее не существует - подгружаем скрипт авторизации;
  • если такая переменная существует, то извлекаем из нее логин, проверяем на корректность и ищем в базе данных пользователя с таким логином, извлекаем всю информацию о нем;
  • если такого пользователя не существует - подгружаем скрипт авторизации;
  • если такой пользователь существует, то формируем из секретного ключа, IP-адреса компьютера и даты последней авторизации строку и вычисляем ее md5-хэш;
  • сравниваем полученную строку с той, которая записана в кукисах;
  • если строки не равны - подгружаем скрипт авторизации, если же равны - отдаем закрытый контент.

практически этот алгоритм может быть реализован следующим образом:

<?php
if (isset($_COOKIE["WebEngineerRestrictedArea"])){
  $data_array = explode(":",$_COOKIE["WebEngineerRestrictedArea"]);
  if (preg_match("/^[a-zA-Z0-9]{3,30}$/", $data_array[0])) {
    $user = $dbconnect->query("SELECT * FROM users WHERE name="".$data_array[0].""");
    $U = $user->num_rows;
    if ($U == 1) {
      $cookies_hash = $data_array[1]; 
      $user_data = $user->fetch_array();
      $evaluate_hash = md5($user_data["secretkey"].":".$_SERVER["REMOTE_ADDR"].":".$user_data["last_login_datetime"]);
      if ($cookies_hash == $evaluate_hash) {
        $access = TRUE;
      } 
    } 
  } else {
    $access = FALSE;
  }
}
 
if (isset($access) and $access = TRUE) {?>
Здесь закрытый контент...
<?php 
  } else {
    include ($_SERVER["DOCUMENT_ROOT"]."/for_article/login.php");
    exit();
  }
?>

таким кодом должны быть снабжены все страницы, содержимое которых необходимо выдать только по логину и паролю.

Таким образом, в случае неизменного IP-адреса машины, клиент получит доступ к материалам на сутки, без необходимости повторной авторизации.

Кнопка “Выход”.

Удобным элементом пользовательского интерфейса для клиента будет кнопка “Выход”, после нажатия на которую - доступ в закрытый раздел снова прекращается и выводится форма авторизации. Для того, чтобы реализовать такую кнопку, достаточно в содержимое закрытой страницы вставить простенькую форму:

<form action="<?php echo $_SERVER["PHP_SELF"];?>" method="post">
<input type="submit" name="exit" value="Выйти"/>
</form>

которая представляет собой всего-навсего одну кнопку “Выйти”. А в начало документа, содержащего закрытый контент, необходимо поместить простой обработчик данных, отправленных с этой формы:

if(@$_POST["exit"]) 
  {
    setcookie("WebEngineerRestrictedArea", "", time()-60*60*24); 
    header ("Location: ".$_SERVER["PHP_SELF"]);
    exit();
  }

Здесь в куки записывается пустое значение, а срок жизни сокращается на сутки, относительно текущего момента, что автоматически делает данную информацию неактуальной. Далее идет перенаправление на текущую страницу, но вместо содержимого закрытого раздела клиент увидит уже форму авторизации.

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

Еще раз о безопасности.

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

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

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

Ссылки для скачивания.

Здесь Вы можете скачать сопроводительные материалы к статье:

avtorizaciya_na_cookies.zip (4Kb)

* * * * * * * * * * * *

Если информация этой статьи будет интересна и полезна Вашему кругу друзей и знакомых, то Вы можете опубликовать ссылку - тогда им проще будет ее найти. Они Вам будут благодарны:).

Комментарии к статье:


Всеволод Чупрыгин © webengineer.pro 2014. Все права защищены.
Копирование материалов сайта разрешено только с указанием имени автора (Всеволод Чупрыгин) и прямой индексируемой ссылки на источник на сайте www.WebEngineer.pro.
ИП Чупрыгин Всеволод Андреевич, ИНН: 333410747832, ОГРН: 311333426300044
http://vkontakte.ru/chuprygin_va, Google +

.
Проверить аттестат
Мы принимаем Webmoney Мы принимаем практически все платежи через Robokassa Мы принимаем Яндекс.Деньги Мы принимаем платежи через QIWI. Мы принимаем платежи через привязанные к QIWI карты VISA/Mastercard.