Форум: Форум PHPФорум ApacheФорум Регулярные ВыраженияФорум MySQLHTML+CSS+JavaScriptФорум FlashРазное
Новые темы: 0000000
Самоучитель MySQL 5. Авторы: Кузнецов М.В., Симдянов И.В. Социальная инженерия и социальные хакеры. Авторы: Кузнецов М.В., Симдянов И.В. PHP. Практика создания Web-сайтов (второе издание). Авторы: Кузнецов М.В., Симдянов И.В. Программирование. Ступени успешной карьеры. Авторы: Кузнецов М.В., Симдянов И.В. PHP 5. На примерах. Авторы: Кузнецов М.В., Симдянов И.В., Голышев С.В.
ВСЕ НАШИ КНИГИ
Консультационный центр SoftTime

Форум PHP

Выбрать другой форум

 

Здравствуйте, Посетитель!

вид форума:
Линейный форум Структурный форум

тема: Мини-статья: самый оптимальный метод авторизации
 
 автор: JIEXA   (31.08.2007 в 18:09)   письмо автору
 
 

Примечание: мини-статья написана для новичков

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

1. Модель (клиент)
Регистрация
- логин (a-z0-9)
- пароль
Вход
- логин
- пароль
Cookie
- уникальный идентификатор юзера
- хэш

Модель (сервер)
MySQL
Таблица users
user_id (int(11))
user_login (Varchar(30))
user_password (varchar(32))
user_hash (varchar(32))
user_ip (int(10)) по умолчанию 0

При регистрации в базу данных записываеться логин пользователя и пароль(в двойном md5 шифровании)
При авторизация, сравниваеться логин и пароль, если они верны, то генерируеться случайная строка, которая хешируеться и добавляеться в БД в строку user_hash. Также записываеться IP адрес пользователя(но это у нас будет опциональным, так как кто-то сидит через Proxy, а у кого-то IP динамический... тут уже пользователь сам будет выбирать безопасность или удобство). В куки пользователя мы записываем его уникальный индетификатор и сгенерированный hash.

Почему надо хранить в куках хеш случайно сгенерированной строки, а не хеш пароля?
1. Из-за невнимательности программиста, во всей системе могут быть дырки, воспользовавшийсь этими дырками, злоумышленик может вытащить хеш пароля из БД и подставить его в свои куки, тем самым получить доступ к закрытым данным. В нашем же случае, двойной хеш пароля не чем не сможет помочь хакеру, так как расшифровать он его не сможет(теоретически это возможно, но на это он потратит не один месяц, а может быть и год) а воспользоваться этим хешем ему негде, ведь у нас при авторизации свой уникальный хеш прикрепленный к IP пользователя.
2. Если злоумышленик вытащит трояном у пользователя уникальный хеш, воспользовать им он также не сможет(разве если только, пользователь решил принебречь своей безопастностью и выключил привязку к IP при авторизации). А вот например, если злоумышленик вытащит cookie у пользователя данного форума(LiteForum), то он тут же получит доступ. Так как разработчики форума, принебригли безопастностью пользователей и решили хранить в куках в открытом ввиде и логин и пароль, что в двойне плохо (узнав пароль пользователя на этом форуме, злоумышленик сможет воспользоваться им и на других рессурсах, на которых зарегистрирован данный пользователь(потому что, как правило пользователи используют один пароль для всего ну или почти для всего).

2. Практика

-- 
-- Структура таблицы `users`
-- 
CREATE TABLE `users` (
  `user_id` int(11) unsigned NOT NULL auto_increment,
  `user_login` varchar(30) NOT NULL,
  `user_password` varchar(32) NOT NULL,
  `user_hash` varchar(32) NOT NULL,
  `user_ip` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY  (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=cp1251 AUTO_INCREMENT=1 ;


register.php

<?
// Страница регситрации нового пользователя

# Соединямся с БД
mysql_connect("localhost""myhost""myhost");
mysql_select_db("testtable");


if(isset(
$_POST['submit']))
{
    
$err = array();

    
# проверям логин
    
if(!preg_match("/^[a-zA-Z0-9]+$/",$_POST['login']))
    {
        
$err[] = "Логин может состоять только из букв английского алфавита и цифр";
    }
    
    if(
strlen($_POST['login']) < or strlen($_POST['login']) > 30)
    {
        
$err[] = "Логин должен быть не меньше 3-х символов и не больше 30";
    }
    
    
# проверяем, не сущестует ли пользователя с таким именем
    
$query mysql_query("SELECT COUNT(user_id) FROM users WHERE user_login='".mysql_escape_string($_POST['login'])."'");
    if(
mysql_result($query0) > 0)
    {
        
$err[] = "Пользователь с таким логином уже существует в базе данных";
    }
    
    
# Если нет ошибок, то добавляем в БД нового пользователя
    
if(count($err) == 0)
    {
        
# Убераем лишние пробелы
        
$login trim($_POST['login']);
        
        
# Убераем лишние пробелы и делаем двойное шифрование
        
$password md5(md5(trim($_POST['password'])));
        
        
mysql_query("INSERT INTO users SET user_login='".$login."', user_password='".$password."'");
        
header("Location: login.php"); exit();
    }
    else
    {
        print 
"<b>При регистрации произошли следующие ошибки:</b><br>";
        foreach(
$err AS $error)
        {
            print 
$error."<br>";
        }
    }
}
?>


<form method="POST">
Логин <input name="login" type="text"><br>
Пароль <input name="password" type="password"><br>
<input name="submit" type="submit" value="Зарегистрироваться">
</form>


login.php

<?
// Страница авторизации


# Функция для генерации случайной строки
function generateCode($length=6) {
    
$chars "abcdefghijklmnopqrstuvwxyzABCDEFGHI JKLMNOPRQSTUVWXYZ0123456789";
    
$code "";
    
$clen strlen($chars) - 1;  //a variable with the fixed length of chars correct for the fence post issue
    
while (strlen($code) < $length) {
            
$code .= $chars[mt_rand(0,$clen)];  //mt_rand's range is inclusive - this is why we need 0 to n-1
    
}
    return 
$code;
}


# Соединямся с БД
mysql_connect("localhost""myhost""myhost");
mysql_select_db("testtable");

if(isset(
$_POST['submit']))
{
    
# Вытаскиваем из БД запись, у которой логин равняеться введенному
    
$query mysql_query("SELECT user_id, user_password FROM users WHERE user_login='".mysql_escape_string($_POST['login'])."' LIMIT 1");
    
$data mysql_fetch_assoc($query);
    
    
# Соавниваем пароли
    
if($data['user_password'] === md5(md5($_POST['password'])))
    {
        
# Генерируем случайное число и шифруем его
        
$hash md5(generateCode(10));
            
        if(!@
$_POST['not_attach_ip'])
        {
            
# Если пользователя выбрал привязку к IP
            # Переводим IP в строку
            
$insip ", user_ip=INET_ATON('".$_SERVER['REMOTE_ADDR']."')";
        }
        
        
# Записываем в БД новый хеш авторизации и IP
        
mysql_query("UPDATE users SET user_hash='".$hash."' ".$insip." WHERE user_id='".$data['user_id']."'");
        
        
# Ставим куки
        
setcookie("id"$data['user_id'], time()+60*60*24*30);
        
setcookie("hash"$hashtime()+60*60*24*30);
        
        
# Переадресовываем браузер на страницу проверки нашего скрипта
        
header("Location: check.php"); exit();
    }
    else
    {
        print 
"Вы ввели неправильный логин/пароль";
    }
}
?>
<form method="POST">
Логин <input name="login" type="text"><br>
Пароль <input name="password" type="password"><br>
Не прикреплять к IP(не безопасно) <input type="checkbox" name="not_attach_ip"><br>
<input name="submit" type="submit" value="Войти">
</form>


check.php

<?
// Скрипт проверки

# Соединямся с БД
mysql_connect("localhost""myhost""myhost");
mysql_select_db("testtable");

if (isset(
$_COOKIE['id']) and isset($_COOKIE['hash']))
{   
    
$query mysql_query("SELECT *,INET_NTOA(user_ip) FROM users WHERE user_id = '".intval($_COOKIE['id'])."' LIMIT 1");
    
$userdata mysql_fetch_assoc($query);

    if((
$userdata['user_hash'] !== $_COOKIE['hash']) or ($userdata['user_id'] !== $_COOKIE['id']) and (($data['user_ip'] == $_SERVER['REMOTE_ADDR'])  or ($data['user_ip'] == "0")))
    {
        
setcookie("id"""time() - 3600*24*30*12"/");
        
setcookie("hash"""time() - 3600*24*30*12"/");
        print 
"Хм, что-то не получилось";
    }
    else
    {
        print 
"Привет, ".$userdata['user_login'].". Всё работает!";
    }
}
else
{
    print 
"Включите куки";
}
?>



Для защиты формы логина от перебора, можно использовать капчу

Если есть вопросы, задавайте их.

   
 
 автор: Binura   (31.08.2007 в 18:44)   письмо автору
 
   для: JIEXA   (31.08.2007 в 18:09)
 

Супер. А почему сессии не использовать? Куки могут быть выкл. В вапе можно вообще о куках забыть...

   
 
 автор: JIEXA   (31.08.2007 в 19:27)   письмо автору
 
   для: Binura   (31.08.2007 в 18:44)
 

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

Для небольших проектов, это конечно не критично... Тут дело вкуса. Я выбираю куки.

По поводу "Куки могут быть выкл". хранить индетификатор сессии в УРЛ тоже не безопасно - пользователь просто скопирует УРЛ и передаст его своему знакомому или разметсит на форуме, тем самым засветив свой индетификатор. Да и те кто выключают куки - намерено идут на это, следовательно это их проблемы.

   
 
 автор: Binura   (31.08.2007 в 19:45)   письмо автору
 
   для: JIEXA   (31.08.2007 в 19:27)
 

Я сначала проверю есть такой сид в бд, а потом запускаю сессию... Удобно выводить онлайн...

   
 
 автор: JIEXA   (31.08.2007 в 19:48)   письмо автору
 
   для: Binura   (31.08.2007 в 19:45)
 

Это да. Но зачем это все. Если можно сделать всё подсебя с помощью кук?

   
 
 автор: Binura   (31.08.2007 в 20:00)   письмо автору
 
   для: JIEXA   (31.08.2007 в 19:48)
 

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

Интересно, если сделать подобие сессии только на мускуле. Будет быстрее работать? :)

   
 
 автор: bronenos   (31.08.2007 в 20:13)   письмо автору
 
   для: Binura   (31.08.2007 в 20:00)
 

имхо, то что написано на С++ в ядре языка, на PHP быстрее не сделать

   
 
 автор: Ralph   (31.08.2007 в 20:34)   письмо автору
 
   для: bronenos   (31.08.2007 в 20:13)
 

Только хотел это написать... :) Работа с сессиями ведется ведь не на уровне ПХП-операторов,а в случае с сессиями в БД кучу рутины (запросы к базе-удаление устаревших сессий,поиск... придется делать именно на уровне ПХП

   
 
 автор: jiraff   (27.10.2007 в 13:47)   письмо автору
 
   для: bronenos   (31.08.2007 в 20:13)
 

> на С++
на чём?

   
 
 автор: Ralph   (31.08.2007 в 20:29)   письмо автору
 
   для: Binura   (31.08.2007 в 20:00)
 

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

   
 
 автор: Binura   (31.08.2007 в 20:34)   письмо автору
 
   для: JIEXA   (31.08.2007 в 19:27)
 

>При большой посещаемости,
- это сколько?

   
 
 автор: Ralph   (31.08.2007 в 20:44)   письмо автору
 
   для: Binura   (31.08.2007 в 20:34)
 

Насчет скорости скажу-месяц назад подобный вопрос поднимался,и я специально провел тест:создал папку с 10.000 файлами с уникальными именами .Так вот,поиск файла СРЕДСТВАМИ ПХП,не на локальном компе,а на сервере хостинг-провайдера (с помощью оператора file_exists )занимал 0.005 секунд.А используя натуральные сессии с обработкой их языком Си...

   
 
 автор: Unkind   (31.08.2007 в 20:45)   письмо автору
 
   для: JIEXA   (31.08.2007 в 19:27)
 

То есть открываеться каталог с сессиями и с каждым файлом сверяеться - нет ли такой сессий.
Данные каталогов кешируются. Не так уж это и долго. К тому же обычно сессии недолго живут.

   
 
 автор: JIEXA   (31.08.2007 в 20:55)   письмо автору
 
   для: Unkind   (31.08.2007 в 20:45)
 

Не буду спорить... мб...
Но в данном материале я рассматривал куки

   
 
 автор: Ralph   (31.08.2007 в 21:07)   письмо автору
 
   для: JIEXA   (31.08.2007 в 20:55)
 

Вот-вот... :) А такие статьи должны отражать не только личные симпатии-антипатии автора,но и все остальные вероятности,а то еще новички подумают,что сессии не есть гуд :-D

   
 
 автор: JIEXA   (31.08.2007 в 21:37)   письмо автору
 
   для: Ralph   (31.08.2007 в 21:07)
 

Я в статье ничего не писал про сессии, рассматривал только куки.

Но опять же хочу подчернуть - я не вижу преимущества сессий.
Единственное их "преимущество" передача индетификатора сессии в URL. А это как я выше писал - не безопасно. Ибо для меня куки куда гибчи сессий. Точнее скажим так - то, что я написал выше уже реализовано в сессиях. Только, тут вы сами выбираете что, где и как хранить....

   
 
 автор: bronenos   (31.08.2007 в 22:18)   письмо автору
 
   для: JIEXA   (31.08.2007 в 21:37)
 

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

   
 
 автор: Binura   (31.08.2007 в 22:35)   письмо автору
 
   для: bronenos   (31.08.2007 в 22:18)
 

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

   
 
 автор: Ralph   (31.08.2007 в 23:03)   письмо автору
 
   для: Binura   (31.08.2007 в 22:35)
 

Не понял,что значит-проверить,вкл ли куки,если нет,то в урл ? При нормальных настройках пхп так и делает-при инициализации новой сессии посылает ид сессии в урл и куках,и если при след.заходе куки пусты,то передает в урл,если нет-в куках...

   
 
 автор: выдр   (01.09.2007 в 20:14)   письмо автору
 
   для: Ralph   (31.08.2007 в 23:03)
 

а при попытке регистрации вылетает вот такая вот
Field 'user_hash' doesn't have a default value
ошибочка =( это при вопытке занести данные в таблицу.

   
 
 автор: JIEXA   (01.09.2007 в 20:38)   письмо автору
 
   для: выдр   (01.09.2007 в 20:14)
 

Какая у вас версия MySQL?
Если интересно, вот здесь http://www.habrahabr.ru/blog/php/24389.html в комментариях есть много интересных мыслей

   
 
 автор: выдр   (01.09.2007 в 21:13)   письмо автору
 
   для: JIEXA   (01.09.2007 в 20:38)
 

версия 5.0... лана, давно пытался себя заставить написать свою авторизацию, у вас возьму пару идей =))

   
 
 автор: Eugene77   (02.09.2007 в 15:57)   письмо автору
 
   для: выдр   (01.09.2007 в 21:13)
 

IP штука ненадёжная и пременчивая в ряде случаев, а coocies
и сессии уязвимы.

Получается в итоге, что надёжной авторизации не существует?!

Что делать?

Может быть программистам объединиться и объявить войну провайдерам с динамическими IP?

Писать юзерам сообщение: "Ваш провайдер ненадёжен так как формирование им динамического IP адреса не позволяет обеспечить безусловную защите ваших данных".

Почитав такое, они, глядишь, пересмотрят свои порядки.

   
 
 автор: bronenos   (02.09.2007 в 20:47)   письмо автору
 
   для: Eugene77   (02.09.2007 в 15:57)
 

это идея =)

   
 
 автор: Unkind   (02.09.2007 в 23:29)   письмо автору
 
   для: Eugene77   (02.09.2007 в 15:57)
 

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

   
 
 автор: Futurer   (03.09.2007 в 18:54)   письмо автору
 
   для: Unkind   (02.09.2007 в 23:29)
 

А я как-то написал скрипт, который обрабатывает все ссылки и формы и добавляет к ним значение ключа. А потом эти ссылки делятся на 2 части:
Формируется форма и общая часть ссылки(без ключа сессии) отсылается в поле action формы, а само значение ключа сессии в скрытом поле методом post в этой же форме. При нажатии на ссылку форма сабмитуется и всё выглядит как клик по ссылке, только ключ не присутствует в строке запроса. Вот вам как вариант.

   
 
 автор: victoor   (03.09.2007 в 18:59)   письмо автору
 
   для: Futurer   (03.09.2007 в 18:54)
 

а что мешает пользователю посмотреть ключ сессии в скрытом поле? делается простым просмотром html-кода...

   
 
 автор: Futurer   (03.09.2007 в 19:06)   письмо автору
 
   для: victoor   (03.09.2007 в 18:59)
 

Я вообще-то это написал тем, кто боится, что пользователь оставит ссылку где-то, в которой будет фигуриорвать его сессия.

А скрывать сессию от самого пользователя, который ей пользуется мне ещё не приходило в голову: зачем? чтобы он сам себя не хакнул? =))

Кстати говоря, в скрытом поле он ничего не увидит, равно как и саму форму, потому что она формируется с использованием метода appendChild DOM2

   
 
 автор: Trianon   (04.09.2007 в 01:28)   письмо автору
 
   для: JIEXA   (31.08.2007 в 18:09)
 

три существенных замечания.

1. magic quotes следует отключить, либо нейтрализовать.
Иначе может случиться неприятность при переносе базы пользователей с одного хостинг на другой, например.

2. Момент генерации случайного ключа лучше бы выразить более четко.
На самом деле вся случайность вызова mt_rand в его неявном обращении к mt_srand - а того, соответственно, к таймеру машины. Все остальное - деетрмированные вычисления.
Лучше прямо взять результат вызова microtime (до определенного знака, считая от младших).

3. Двойной хеш, конечно, лучше одинарного, но не на порядок.
Лучше генерировать Init Vector для каждого эккаунта (тот же случайный ключ), сохранять его в таблице, и хешировать пароль с его учетом и с учетом логина. Тогда атака построением таблиц хешей явно загнется, останутся лишь брут форс и словарь.

Еще лучше, конечно, аутентифицироваться по challenge-response методике, но для этого потребуется считать хеш на клиенте... В принципе, если клиент поддерживает хотя бы JS - это реально, хотя и крайне муторно. Тут и до https недалеко, тем более, что для обеспечения хотя бы коммерческого уровня секьюрности он всё равно потребуется.

PS. А умалчиваемое значение полю user_hash всё же стоит задать. Версия MySQL тут не при чем. Если у поля стоит NOT NULL и не стоит DEFAULT - оператор INSERT должен задавать значение такого поля явно.

   
 
 автор: JIEXA   (27.10.2007 в 04:22)   письмо автору
 
   для: Trianon   (04.09.2007 в 01:28)
 

Собирусь как-нить, напишу немного в другом виде. Есть пара идей

   
 
 автор: DiMoN_TD   (29.11.2007 в 05:24)   письмо автору
 
   для: JIEXA   (31.08.2007 в 18:09)
 

Вы мне можете объяснить сл. строки в коде check.php:

    if(($userdata['user_hash'] !== $_COOKIE['hash']) or ($userdata['user_id'] !== $_COOKIE['id']) and (($data['user_ip'] == $_SERVER['REMOTE_ADDR'])  or ($data['user_ip'] == "0"))) 
    { 
        setcookie("id", "", time() - 3600*24*30*12, "/"); 
        setcookie("hash", "", time() - 3600*24*30*12, "/"); 
        print "Хм, что-то не получилось"; 
    } 
    else 
    { 
        print "Привет, ".$userdata['user_login'].". Всё работает!"; 
    }


До меня что-то не доходит, почему, если все эти условия выполняются, то у нас что-то не получилось... может наоборот, у нас всё прекрасно, всё совпало и можно идти дальше??
А то я в этих кукисам дуб дубом =(

   
 
 автор: DiMoN_TD   (02.12.2007 в 00:03)   письмо автору
 
   для: JIEXA   (31.08.2007 в 18:09)
 

ну так что, никто не может мне помочь?

   
 
 автор: Trianon   (02.12.2007 в 00:34)   письмо автору
 
   для: DiMoN_TD   (02.12.2007 в 00:03)
 

В условиях стоят знаки неравенства.

   
 
 автор: DiMoN_TD   (02.12.2007 в 01:40)   письмо автору
 
   для: Trianon   (02.12.2007 в 00:34)
 

Ужо разобрался, но на самом деле в этой статье множество ошибок, до которых пришлось мне, как нубу, догадываться долго и нудно =(

   
Rambler's Top100
вверх

Rambler's Top100 Яндекс.Метрика Яндекс цитирования