Другие разделы

Правила

FAQ

Пользователи

Комментарии

Друзья сайта

Flash Loops

RaceBook

Безопасная аутентификация пользователей с помощью PHP, MySQL, JS, Cookie, Session (версия 3)

Предыдущий алгоритм получился хорошим, но у него одна уязвимость - от клиента к серверу передаётся пароль в виде хеша (без соли). В новом алгоритме эта проблема решена.

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

Был придумал алгоритм, который не передаёт пароль в открытом виде или же в "чистом" хешированном виде. Пароль передаётся в хешированном виде с солью, а соль всегда динамическая, но при этом он действует в тот момент времени, пока не "дойдёт" до БД, после этого он уже становится не активным.

После того как аутентификация прошла успешно, то пароль вообще ни в каком виде уже ни куда не передаётся.

Теперь по порядку, как же всё это работает (алгоритм опишу, при правильном вводе логина и пароля):

  1. Пользователь заполняет форму с логином и паролем;
  2. При нажатии на кнопку "войти" - срабатывает JS алгоритм, который посредством AJAX отправляет на сервер только логин, то есть без пароля;
  3. На сервере обрабатывается (регулярными выражениями) логин, после чего в БД ищется данный логин;
  4. После того как логин был найден, то создаётся переменная с текущим временем в секундах (UNIX), которая прописывается в запись в БД к найденному логину.
  5. Сервер возвращает результат (время) в JS;
  6. Пароль из формы хешируется с солью, а в виде соли у нас полученное время;
  7. На сервер отправляется логин в открытом виде и хешированный пароль;
  8. На сервере опять происходит обработка логина и хешированного пароля, после чего ищется запись в БД по логину (проверяется так же не пустота поля времени в БД);
  9. Пароль из БД (уже хешированный, но без соли) хешируется с солью, которая находится в этой же записи в БД (пункт 4, соль была записана в БД);
  10. Идёт сравнение хешированных паролей;
  11. Поле с временем (солью) в БД очищается;
  12. Формируется рандомное число;
  13. Шифруется id пользователя с помощью рандомного числа (допустим умножение);
  14. Шифрованный id записывается в куки (cookie);
  15. В сессию записывается логин и рандомное число;

Всё, пользователь прошёл аутентификацию, а далее, при переходах на другие страницы расшифровывается id с помощью рандомного числа из сессии, ищется логин в БД, сверяется расшифрованный id, после чего формируется новое рандомное число и снова шифруется id, то есть в итоге мы получаем динамический id (шифрованный) в куках при каждом переходе.

Переходим от теории к практике :)

В БД пароль хранится в виде двойного хеша md5.

БД:

  1. `id` INT(11) NOT NULL AUTO_INCREMENT,
  2. `login` VARCHAR(20) COLLATE utf8_unicode_ci NOT NULL,
  3. `hash` VARCHAR(32) COLLATE utf8_unicode_ci NOT NULL
  4. `time` BIGINT(10) DEFAULT NULL

Необходимые файлы, которые будут подгружаться в алгоритме:

function.php:

  1. function rs($parameter,$text,$count_symbol){
  2.     return mb_substr(mb_ereg_replace($parameter,'',$text),0,$count_symbol,'UTF-8');
  3. }
  4. function message_js($text){
  5.     return '<script language="JavaScript">alert("'.$text.'");</script>';
  6. }

db.php:

  1. $db_my=mysql_connect('localhost','root','password') or die('нет конекта к базе данных');
  2. mysql_select_db('db_name',$db_my) or die('нет конекта к выбранной базе');
  3. mysql_query("SET NAMES UTF8");

md5.html - здесь находится алгорим md5 на JS, посмотреть можно здесь http://javascript.ru/php/md5

К форме с аутентификацией подгружаем файлы:

  1. require_once './md5.html';
  2. require_once './js_auth.html';

Форма:

  1. <form action="./" method="post" onsubmit="ajax(this);return false;">
  2. <p>Логин:</p>
  3. <input type="text" name="login" id="login" maxlength="20">
  4. <p>Пароль:</p>
  5. <input type="password" name="password" maxlength="32" id="password">
  6. <input type="hidden" name="enter" value="y">
  7. <input type="submit" id="button" class="button" value="Войти">
  8. </form>

Файл js_auth.html:

  1. <script type="text/javascript">
  2. function ajax(forma){
  3.     if(window.XMLHttpRequest){
  4.         xmlhttp=new XMLHttpRequest();
  5.     }
  6.     else{
  7.         xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  8.     }
  9.     xmlhttp.onreadystatechange=function(){
  10.         if(xmlhttp.readyState==1){
  11.             document.getElementById('button').value='Пожалуйста подождите...';
  12.         }
  13.         if(xmlhttp.readyState==4 && xmlhttp.status==200){
  14.             var str=xmlhttp.responseText;
  15.             if(str!='error'){
  16.                 document.getElementById('password').value=hex_md5(hex_md5(hex_md5(document.getElementById('password').value))+str);
  17.                 forma.submit();
  18.             }
  19.             else{
  20.                 document.getElementById('button').value='Войти';
  21.                 alert('Такой комбинации логина и пароля не существует.');
  22.             }
  23.         }
  24.     }
  25.     in_post='login='+document.getElementById('login').value;
  26.     xmlhttp.open("POST","./ajax_auth.php",true);
  27.     xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded;charset=UTF-8");
  28.     xmlhttp.send(in_post);
  29. }
  30. </script>

Файл ajax_auth.php:

  1. require_once './function.php';  //различные функции
  2. $login=rs('[^A-Za-z]',$_POST['login'],40);  //удаление запрещённых символов
  3. if(!empty($login)){
  4.     require_once './db.php';    //подгружаем файл для подключения к БД
  5.     $login_row=mysql_fetch_array(mysql_query("
  6.         SELECT
  7.             `id`
  8.         FROM
  9.             users
  10.         WHERE
  11.             `login`='".$login."'
  12.     "));
  13.     if(!empty($login_row['id'])){   //если юзер был найден
  14.         $time=time();       //прописываем в переменную текущую дату и время в секундах
  15.         mysql_query("
  16.             UPDATE
  17.                 users
  18.             SET
  19.                 `time`='".$time."'
  20.             WHERE
  21.                 `id`='".$login_row['id']."'
  22.         ");
  23.         echo $time; //возврат времени в js
  24.     }
  25.     else{   //иначе возврат ошибки
  26.         echo 'error';
  27.     }
  28.     unset($time,$login_row,$login); //удаление переменных
  29.     mysql_close($db_my);        //закрываем конект к БД
  30. }
  31. else{
  32.     echo 'error';   //возвращаем ошибку в js, если поле логина пустое или оно стало пустым после обработки
  33. }

И наконец сам алгоритм проверки аутентификации, размещается перед формой, и если аутентификация проходит, то форму не выводим:

  1. require_once './db.php';
  2. if(isset($_POST['enter'])){ //Если были отправлены данные с формы входа
  3.     $login=rs('[^A-Za-z]',$_POST['login'],20);
  4.     $hash=rs('[^A-Za-z0-9]',$_POST['password'],32);
  5.     if(!empty($login) && !empty($hash)){    //если переменные логина и хэша не пустые после удаления запрещённых символов, то идём дальше
  6.         $user_row=mysql_fetch_array(mysql_query("
  7.             SELECT
  8.                 `id`,
  9.                 `hash`,
  10.                 `time`
  11.             FROM
  12.                 users
  13.             WHERE
  14.                 `login`='".$login."'
  15.         "));
  16.         if(!empty($user_row['time'])){  //если найденый юзер с непустым полем времени, то идём дальше
  17.             $check_hash=md5($user_row['hash'].$user_row['time']);   //формируем проверяющий хэш, чтобы сравнить с присланным из формы
  18.             if($check_hash==$hash){ //проверка хэша
  19.                 mysql_query("
  20.                     UPDATE
  21.                         users
  22.                     SET
  23.                         `time`=''
  24.                     WHERE
  25.                         `id`='".$user_row['id']."'
  26.                 ");
  27.                 $random_number=mt_rand(2,100);                  //формируем рандомное число
  28.                 $crypt_id=$random_number*($user_row['id']+$random_number);  //шифруем айдишник
  29.                 setcookie('id',$crypt_id);                  //записываем зашифрованный айдишник в куки
  30.                 session_start();                        //стартуем сессию
  31.                 $_SESSION['login']=$login;                  //записываем логин в сессию
  32.                 $_SESSION['random_number']=$random_number;          //записываем рандомное число в сессию
  33.                 define('__user_id',$user_row['id']);                //создаём константу с айдишником юзера
  34.                 unset($random_number,$crypt_id);                //удаление переменных
  35.             }
  36.             else{
  37.                 echo message_js('Такой комбинации логина и пароля не существует.'); //косяк, пароль не совпал
  38.             }
  39.             unset($check_hash);
  40.         }
  41.         else{
  42.             echo message_js('Такой комбинации логина и пароля не существует.'); //косяк, пользователь в БД не найден
  43.         }
  44.         unset($user_row);   //удаление массива с инфой о юзере из БД
  45.     }
  46.     else{
  47.         echo message_js('Такой комбинации логина и пароля не существует.'); //косяк, логин или хэй пришли пустые из формочки
  48.     }
  49.     unset($login,$hash);    //удаление переменных
  50. }
  51. else{
  52.     session_start();    //стартуем сессию
  53.     if(isset($_COOKIE['id']) && isset($_SESSION['login']) && isset($_SESSION['random_number'])){    //если куки и сессия не пустые
  54.         $login=rs('[^A-Za-z]',$_SESSION['login'],20);
  55.         $random_number=rs('[^0-9]',$_SESSION['random_number'],3);
  56.         $id_crypt=rs('[^0-9]',$_COOKIE['id'],10);
  57.         $id_user=($id_crypt/$random_number)-$random_number; //дешифруем полученный айдишник из куки с полученным рандомным числом из сессии
  58.         $user_row=mysql_fetch_array(mysql_query("
  59.             SELECT
  60.                 `id`
  61.             FROM
  62.                 users
  63.             WHERE
  64.                 `login`='".$login."' and
  65.                 `id`='".$id_user."'
  66.         "));
  67.         if(!empty($user_row['id'])){    //если юзер найден
  68.             $random_number=mt_rand(2,100);                  //формируем рандомное число
  69.             $crypt_id=$random_number*($user_row['id']+$random_number);  //шифруем айдишник
  70.             setcookie('id',$crypt_id);                  //прописываем зашифрованный айдишник в куки
  71.             $_SESSION['login']=$login;                  //записываем логин в сессию
  72.             $_SESSION['random_number']=$random_number;          //записываем рандомное число в сессию
  73.             define('__user_id',$user_row['id']);                //создаём константу с айдишником юзера
  74.             unset($crypt_id);                       //удаляем переменую
  75.         }
  76.         else{   //если юзер не найден
  77.             setcookie('id','');                 //очищаем куки
  78.             unset($_SESSION);                   //удаляем сессию
  79.             echo message_js('ошибка... сообщите администратору');   //косяк, скорее всего страницу обновляли быстро
  80.         }
  81.         unset($login,$random_number,$id_crypt,$id_user,$user_row);  //удаление переменных
  82.     }
  83. }

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

Обсудить на форуме

© Филимошин В. Ю., 2013

Комментарии

А вообще...мне тут спец один на форруме пхп.су разъяснил кое-что:

1)Такие вещи, как шифрованный ид вообще не надо использовать, и даже надо делать не совсем как я предложил - генерить уникальный ид для каждого юзера (динамический, как вы и говорили). Тут надо использовать токены - динанический (Псевдо)случайный набор символов, уникальный для всей системы. Я предложил md5(uniqid(', true)). Горорит нет -надо, чтобы криптоустойчив. Решение есть в пхп 5.3 и более - ф-ция openssl_random_pseudo_bytes.
2)Хранить в бд мд5(пасс) - ненадежно, сольют бажу - будут иметь все и всех... Решение - password_hash - пхп5.5
3)И самое главное - все это...и блофиш в том числе - не устоят против атаки MitM (человек посередине). "И никак вы от подмены куки не защититесь. Т.к. HTTP не предполагает сессии в принципе. Единственное достоверное, что мы знаем о пользователе - его IP. Но к нему привязывать нельзя."... А на вопрос куда бежать и что делать)) говорит : "Разумеется, SSL. Один только алгоритм рукопожатия чего стоит.".
Так самопальный ссл сертификат, говорю, браузер постоянно материть будет...На что об мне две ссылки дал на бесплатные, но официальные сертификаты стартссл
http://habrahabr.ru/post/106252/
http://habrahabr.ru/post/127643/

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

05:16:36 | 06/06/2014

Спасибо за столь развёрнутые комментарии.
Под кражей сессии я конечно же имел ввиду идентификатор, выразился не совсем верно.
Статью про Blowfish прочту попозже и буду обдумывать новый какой то алгоритм :)

13:36:08 | 05/06/2014

P.S. Хочу еще добавить что идея с динамик ид имеет основной целью не повышения безопасности (против кражи куки), а идентификацию юзера для последующей автоматической перерегистрации (при этом скрывается сам юзер - каждый раз он предстает в новом свете - мне понравилось...искал нечто подобное)) ). На многих сайтах на странице авторизации ясть галочка - запомнить вход. Именно для этого. Когда сессия закрывается, чтобы юзер остался в системе - нужно открыть новую сессию. Реализация - мы записываем этот динамик ид (а также данные для защиты от подделку куки - айпи, браузер, что-то еще...). Когда видим - сесси нет - проверяем динам ид, айпи и прочее. Совпало - открываем новую сессию и записываем в нее эти данные из бд. Вуаля - юзер снова в системе.

06:01:06 | 04/06/2014

Для начала оговорюсь, я не претендую на звание мастера веб-программирования, но...
У вас тут была фраза: "если не будет логина в сессии и если вдруг кто-то её перехватит". Сессию перехватит?!! Сессию нельзя перехватить. Эти данные хранятся на сервере и не пересылаются клиенту (если только вы сами это преднамеренно не сделали).
А уж если взломали ваш сервер...То у них будет не только сессия - но доступ к вашим скриптам - и Ваш алгоритм...шифрования ид - зачит и ид (Да он просто модифицирует ваши скрипты или зальет свои...и получит все, что ему надо). Да что тут говорить - там уж и до бд недалеко (особенно если под бд, как и у большинства (и у меня тоже =) - пока что неоправданно затратно иметь выделенный серв бд), не выделен отдельный сервер).
Можно перехватить лишь идентификатор сессии (и достаточно несложно) и подставить его себе. Поставил ид сессии себе - и ты уже тот юзер, у которого ты их украл.
В вашем случае: просто берутся куки юзера и ставятся злоумышленнику. Итог - у него в куки - ид сессии, по которому сервер его опознает, и зашифрованный ид. Ему вовсе не требуется его расшифровывать и вообще знать - в данном случае ваш скрипт (с такими то подставленными данными в куки) просто прогонит его через себя, признает его за старого доброго юзера - и выдаст ему необходимые права.
Тут у Вас есть небольшая защита против вышеперечисленного - динамик ид. Идея неплохая. Но если злоумышленник не успел с куками ид, он просто заново их украдет, пока ид не устарел, и через раз у него получится.
Тут главная задача - сделать так, чтобы юзер не просто получал сессию по хранимому ид сессии в куки, а до этого скриптом еще проверялись другие параметры...браузер, ай-пи (свои плюсы: хорошая защита от подстановки куки, и минусы: ситуация с прокси и динамич айпи. Тоже решаемо - можно ставить выбор пользователю - галку безопасный вход...или брать только адрес сети, как правило - первые 3 знака). Если данные не совпадают - чистим сессию, сохраняем в логи айпи потенциального злоумышленника (можно действия и поактивнее - добавление айпи в черный список, но черевато - в случае динамич айпи можно поставить под блокировку зашедших поздее с этого ип и других юзеров). Настоящему юзеру нужно только перезайти. Да и капчу можно вывести... Все это даст увеличение (не всегда значительное, но увеличение) безопасности сайта.

Если вы заинтересованы в максимальном затруднении отслеживания юзера по его логину...или там ид - могу посоветовать Blowfish.
http://habrahabr.ru/sandbox/20718/

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

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

05:50:33 | 04/06/2014

По моему вы не до конца поняли суть данного алгоритма. Во-первых, если не будет логина в сессии и если вдруг кто-то её перехватит и в куки подставит любое число и получится так, что расшифровка пройдёт "Успешно", то есть образуется id реального юзера... ну там уже понятно... Во-вторых, если в куках хранить просто id пользователя и хоть как его там защитить, то вытащив эти куки и подставив к себе... опять понятно...
На счёт дороботки сайта - знаю все эти косяки и доработка пока что не планируется...

13:43:55 | 03/06/2014

P.S. По-моему...Вам нужно слегка доработать Ваш сайт - при обновлении страницы (с которой оставлял комментарий), каждый раз дублируется сообщение (окно сообщения уже пустое...). Дублирование прекращается только после ввода в новом окне адреса страницы...

12:44:26 | 03/06/2014

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

12:40:40 | 03/06/2014

Да, всё верно вы пишите, но не каждый сможет это проделать :) на счёт ssl - не всегда удаётся его использовать, а данный алгоритм дорабатывается, так что ждите 4 версию :)

04:37:12 | 17/05/2014

давайте уточним:алгоритм хеширования пароля я могу посмотреть на стороне клиента и меня удерживает только незнания какая соль... но что мне мешает узнать соль в момент передачи ответа от сервера (Сервер возвращает результат (время) в JS;), а зная соль, алгоритм шифрования и уже хешированный пароль я узнаю пароль...не проще использовать SSL серификаты для защиты соединения?

20:04:26 | 14/05/2014

Nabla, в статье есть исходники, копируйте и правьте. Если исходники не в виде файла, то это уже не исходники что ли по вашему?

10:05:34 | 05/02/2014

Где обещанные исходники?

20:30:52 | 04/02/2014

Вход

Логин:

Зарегистрировать

Пароль:

Забыли пароль?

запомнить

Пятнашки

1
4
2
5
13
12
9
3
7
6
11
8
14
10
15

Опрос

Как Вы узнали об этом сайте?

От админа (1035)

48.4%

От друзей (32)

1.49%

Поисковик выдал... (983)

45.9%

Увидел(а) на других сайтах (46)

2.15%

Другое (41)

1.91%

Случайное фото

Слотовый Pentium !!! радиатор

Счётчики

Яндекс.Метрика

Internet Map

Каталог@Mail.ru - каталог ресурсов интернет