|
|
|
| Примечание: мини-статья написана для новичков
Давайте посмотрим вокруг: форумы, интернет магазины, гостевые книги и т.д. используют регистрацию и последующую авторизацию пользователей. Можно даже сказать, что это почти необходимая функция каждого сайта (только если это не домашняя страничка Васи Пупкина или не визитная карточка, какой-нибудь небольшой компании). Сегодня я хочу поделиться со всеми новичками информацией, о том, как лучше это все реализовать.
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']) < 3 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($query, 0) > 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", $hash, time()+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 "Включите куки";
}
?>
|
Для защиты формы логина от перебора, можно использовать капчу
Если есть вопросы, задавайте их. | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 18:09)
| | Супер. А почему сессии не использовать? Куки могут быть выкл. В вапе можно вообще о куках забыть... | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 18:44)
| | При большой посещаемости, сессии отрицательно сказываються на производительности.
А точнее при генерации уникального индетификатора сессии происходит проверка, на сущесвование сего. То есть открываеться каталог с сессиями и с каждым файлом сверяеться - нет ли такой сессий.
Для небольших проектов, это конечно не критично... Тут дело вкуса. Я выбираю куки.
По поводу "Куки могут быть выкл". хранить индетификатор сессии в УРЛ тоже не безопасно - пользователь просто скопирует УРЛ и передаст его своему знакомому или разметсит на форуме, тем самым засветив свой индетификатор. Да и те кто выключают куки - намерено идут на это, следовательно это их проблемы. | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 19:27)
| | Я сначала проверю есть такой сид в бд, а потом запускаю сессию... Удобно выводить онлайн... | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 19:45)
| | Это да. Но зачем это все. Если можно сделать всё подсебя с помощью кук? | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 19:48)
| | Куки отключают, в мобильном интернете еще хуже. Некоторые сотовые не поддерживают куки... Из-за этого терять людей...
Интересно, если сделать подобие сессии только на мускуле. Будет быстрее работать? :) | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 20:00)
| | имхо, то что написано на С++ в ядре языка, на PHP быстрее не сделать | |
|
|
|
|
|
|
|
для: bronenos
(31.08.2007 в 20:13)
| | Только хотел это написать... :) Работа с сессиями ведется ведь не на уровне ПХП-операторов,а в случае с сессиями в БД кучу рутины (запросы к базе-удаление устаревших сессий,поиск... придется делать именно на уровне ПХП | |
|
|
|
|
|
|
|
для: bronenos
(31.08.2007 в 20:13)
| | > на С++
на чём? | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 20:00)
| | Не знаю,я все время работаю только с мобильным интернетом,так мой Мотор С450 легко держал куки...Учитывая,что дохрена людей ходят с Мини-Оперы (она сама поддерживает куки),много людей вы не потеряете | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 19:27)
| | >При большой посещаемости,
- это сколько? | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 20:34)
| | Насчет скорости скажу-месяц назад подобный вопрос поднимался,и я специально провел тест:создал папку с 10.000 файлами с уникальными именами .Так вот,поиск файла СРЕДСТВАМИ ПХП,не на локальном компе,а на сервере хостинг-провайдера (с помощью оператора file_exists )занимал 0.005 секунд.А используя натуральные сессии с обработкой их языком Си... | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 19:27)
| | То есть открываеться каталог с сессиями и с каждым файлом сверяеться - нет ли такой сессий.
Данные каталогов кешируются. Не так уж это и долго. К тому же обычно сессии недолго живут. | |
|
|
|
|
|
|
|
для: Unkind
(31.08.2007 в 20:45)
| | Не буду спорить... мб...
Но в данном материале я рассматривал куки | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 20:55)
| | Вот-вот... :) А такие статьи должны отражать не только личные симпатии-антипатии автора,но и все остальные вероятности,а то еще новички подумают,что сессии не есть гуд :-D | |
|
|
|
|
|
|
|
для: Ralph
(31.08.2007 в 21:07)
| | Я в статье ничего не писал про сессии, рассматривал только куки.
Но опять же хочу подчернуть - я не вижу преимущества сессий.
Единственное их "преимущество" передача индетификатора сессии в URL. А это как я выше писал - не безопасно. Ибо для меня куки куда гибчи сессий. Точнее скажим так - то, что я написал выше уже реализовано в сессиях. Только, тут вы сами выбираете что, где и как хранить.... | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 21:37)
| | пусть это останется индивидуальным выбором каждого
в конце концов, в чужой монастырь... | |
|
|
|
|
|
|
|
для: bronenos
(31.08.2007 в 22:18)
| | Ну можно проверять вкл. или нет куки... Если нет то в урл. Перед выводом сообщений, проверять на наличие ссылок и сид в них... Так же проверять на реферер... Т.е. игнорировать авторизацию при переходе с других сайтов. Если я не ошибаюсь, даже если написать в адресную строку ссылку, реферер исчезает. | |
|
|
|
|
|
|
|
для: Binura
(31.08.2007 в 22:35)
| | Не понял,что значит-проверить,вкл ли куки,если нет,то в урл ? При нормальных настройках пхп так и делает-при инициализации новой сессии посылает ид сессии в урл и куках,и если при след.заходе куки пусты,то передает в урл,если нет-в куках... | |
|
|
|
|
|
|
|
для: Ralph
(31.08.2007 в 23:03)
| | а при попытке регистрации вылетает вот такая вот
Field 'user_hash' doesn't have a default value
ошибочка =( это при вопытке занести данные в таблицу. | |
|
|
|
|
|
|
|
для: выдр
(01.09.2007 в 20:14)
| | Какая у вас версия MySQL?
Если интересно, вот здесь http://www.habrahabr.ru/blog/php/24389.html в комментариях есть много интересных мыслей | |
|
|
|
|
|
|
|
для: JIEXA
(01.09.2007 в 20:38)
| | версия 5.0... лана, давно пытался себя заставить написать свою авторизацию, у вас возьму пару идей =)) | |
|
|
|
|
|
|
|
для: выдр
(01.09.2007 в 21:13)
| | IP штука ненадёжная и пременчивая в ряде случаев, а coocies
и сессии уязвимы.
Получается в итоге, что надёжной авторизации не существует?!
Что делать?
Может быть программистам объединиться и объявить войну провайдерам с динамическими IP?
Писать юзерам сообщение: "Ваш провайдер ненадёжен так как формирование им динамического IP адреса не позволяет обеспечить безусловную защите ваших данных".
Почитав такое, они, глядишь, пересмотрят свои порядки. | |
|
|
|
|
|
|
|
для: Eugene77
(02.09.2007 в 15:57)
| | это идея =) | |
|
|
|
|
|
|
|
для: Eugene77
(02.09.2007 в 15:57)
| | Полнейшая чушь. Пользователи спокойно покинут Ваш ресурс и забудут о нем навсегда. Что, в принципе, правильно. | |
|
|
|
|
|
|
|
для: Unkind
(02.09.2007 в 23:29)
| | А я как-то написал скрипт, который обрабатывает все ссылки и формы и добавляет к ним значение ключа. А потом эти ссылки делятся на 2 части:
Формируется форма и общая часть ссылки(без ключа сессии) отсылается в поле action формы, а само значение ключа сессии в скрытом поле методом post в этой же форме. При нажатии на ссылку форма сабмитуется и всё выглядит как клик по ссылке, только ключ не присутствует в строке запроса. Вот вам как вариант. | |
|
|
|
|
|
|
|
для: Futurer
(03.09.2007 в 18:54)
| | а что мешает пользователю посмотреть ключ сессии в скрытом поле? делается простым просмотром html-кода... | |
|
|
|
|
|
|
|
для: victoor
(03.09.2007 в 18:59)
| | Я вообще-то это написал тем, кто боится, что пользователь оставит ссылку где-то, в которой будет фигуриорвать его сессия.
А скрывать сессию от самого пользователя, который ей пользуется мне ещё не приходило в голову: зачем? чтобы он сам себя не хакнул? =))
Кстати говоря, в скрытом поле он ничего не увидит, равно как и саму форму, потому что она формируется с использованием метода appendChild DOM2 | |
|
|
|
|
|
|
|
для: 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 должен задавать значение такого поля явно. | |
|
|
|
|
|
|
|
для: Trianon
(04.09.2007 в 01:28)
| | Собирусь как-нить, напишу немного в другом виде. Есть пара идей | |
|
|
|
|
|
|
|
для: 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'].". Всё работает!";
}
|
До меня что-то не доходит, почему, если все эти условия выполняются, то у нас что-то не получилось... может наоборот, у нас всё прекрасно, всё совпало и можно идти дальше??
А то я в этих кукисам дуб дубом =( | |
|
|
|
|
|
|
|
для: JIEXA
(31.08.2007 в 18:09)
| | ну так что, никто не может мне помочь? | |
|
|
|
|
|
|
|
для: DiMoN_TD
(02.12.2007 в 00:03)
| | В условиях стоят знаки неравенства. | |
|
|
|
|
|
|
|
для: Trianon
(02.12.2007 в 00:34)
| | Ужо разобрался, но на самом деле в этой статье множество ошибок, до которых пришлось мне, как нубу, догадываться долго и нудно =( | |
|
|
|