Может, конечно, я буду и не прав, доказывая, что механизм сессий, реализованный на PHP или Perl, на самом деле очень простой и заключается в обычной передаче уникального ключа между пользователем и сервером (и обратно), на основе которого скрипт выбирает из памяти сервера массив данных для этого ключа (обычно это простые текстовые файлы в специальной папке). Уникальный ключ может передаваться как минимум тремя способами: посредством cookies и в URL как часть домена или переменная окружения. При этом способ передачи особой роли не играет. Так же сессия может (а по большей части обязана) привязываться к другим переменным окружения передаваемым пользователем, таким как, например: IP адрес и прокси-сервер.
Бывает так, что из-за неправильной настройки сервера или же из-за отсутсвия толкового админа стандартные механизмы сессий не работают. Бывает что начинающие пользователи просто не могут понять, что это за механизм и как его использовать. В общем, всякое бывает...
В данной статье мы создадим и рассмотрим собственный механизм сессий (кому не нравится слово "сессия", могут читать его как "авторизация"), точнее, определим основную концепцию механизма для дальнейшей её доработки.
Итак, какие ресурсы нам понадобятся (за исключением рук и головы):
- База данных MySQL (либо либая другая SQL БД);
- Интерпритатор Perl и предустановленные модули:
- CGI - для разбора переменных окружения;
- CGI::Cookies - для разбора cookies (входит в состав CGI);
- DBI - интерфейс для работы с базой данных;
- DBD::mysql - драйвер для работы с MySQL базой данных (либо какой другой для соответствующий БД);
- Storable - модуль для создания и хранения "дампов" данных (либо какой другой, например: Data::Dumper);
- Digest::MD5 - модуль для шифрования паролей;
- Сайт, в который требуется внедрить авторизацию.
Сайт может быть как динамичным так и статичным, в любом случае система будет состоять из 4-х элементов (скриптов):
-
Элемент - форма приветствия - выводит на всех страницах сайта небольшую форму авторизации (логин-пароль), если пользователь не авторизирован, или блок приветсвия (Здравствуй Вася!!!), если пользователь уже авторизирован. Так как данный элемент находится практически на всех страницах сайта, то он будет внедряться в готовые HTML страницы и скрипты (способы внедрения будут описаны ниже);
-
Элемент - скрипт входа - получает из формы авторизации логин и пароль, проверяет их на соответсвие и возвращает результат в виде приветсвия, рефера или ошибки;
- Элемент - скрипт выхода - обнуляет (сбрасывает) сессию пользователя;
- Элемент - скрипт регистрации - форма регистрации пользователя;
Определим принцип работы скриптов;
Авторизацию будем производить через Cookies, можно, конечно, заострить внимание на то, что многие пользователи отключают Cookies (поверьте - это не так, многие пользователи даже не подозревают о их существовании), можно сказать, что они работают зачастую криво, в общем можно много что рассказать против, но при этом альтернативы не менее "глючные". Принцип простой: как только пользователь первый раз выходит на сайт, в базе данных создается гостевая сессия в ввиде уникального ключа и она же прописывается пользователю в Cookies. В дальнейшем, если пользователь авторизируется через форму или регистрируется его гостевая сессия изменяется на пользовательскую, при этом сам уникальный ключ остается неизменным для дого что бы не передавать повторно Cookies. При выходе (принудительное снятие авторизации, а не простое закрытие браузера) соответствующая запись в таблице сессий изменяется на гостевую, при этом Cookies пользователя - не изменяется. Во время регистрации создается новая запись в таблице пользователей, а в таблице сессий гостевая сессия привязывается к вновь созданному пользователю.
Для хранения дампов информации, создадим папку data. Информация о сессии и информация о пользователе (пользовательская информация) хранятся раздельно. Это сделано для того, что бы дать возможность пользователям автоматически переносить свои настройки при изменении рабочего места (например: собрав заказ или сделав закладки на сайте на работе пользователь может прийти домой и во время авторизации его на сайте все его настройки остаются в силе). Данные о сессии мы будем хранить в баззе данных так как структура жестко определена, а пользовательские данные (не информация а пользователе) с помощью модуля Storable так как структура данных может быть разной для каждого пользователя.
Для того что бы Cookies пользователя было проблематично использовать на других компьютерах, если его кто-нибудь "утащит", мы привяжем сессию к IP адресу, хосту и прокси серверу. Хотя следует учесть, что пользователь при выходе с dial-up во время реконнекта может изменить свой IP адрес, что приведет к несоответсвию сессии и потере авторизации, для этого IP адрес пользователя сократим до первых двух чисел, которые при реконнекте dial-up соединения меняются в исключительных случаях.
Определим структуру базы данных, она состоит из двух таблиц: таблицы сессий и таблицы пользователей:
CREATE TABLE session ( session CHAR(32) NOT NULL, user INT(11) NOT NULL, time INT(11) NOT NULL, host VARCHAR(100) NOT NULL, ip VARCHAR(100) NOT NULL, forwarded VARCHAR(100) NOT NULL, PRIMARY KEY (session) );
CREATE TABLE users ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, pass VARCHAR(100) NOT NULL, info VARCHAR(250) NOT NULL, PRIMARY KEY (id) );
И сразу же внесем в таблицу пользователя гостя и запомним id (= 4) для указания его в скриптах:
INSERT INTO users SET id = 4, name = 'Guest', pass = 'nopassword';
1. Элемент форма приветсвия
Самый сложный скрипт относительно остальных, не сколько по структуре, как по внедрению на сайт. Если остальные скрипты легко переносимые и не требуют дополнительных привязок, то этот требует определенной "возни". На статичных страницах данный скрипт внедряется с помощью SSI, на динамичных страницах, код скрипта внедряется: либо в код основного скрипта, либо пишется отдельный модуль, либо используется как внешняя процедура (при использовании requere). Мы рассмотрим вариант внедрения в статичные страницы (другие варианты не рассматриваю в виду того, что код приветсвия может внедряться в совершенно разные по структуре скрипты, главное, что алгоритм остается неизменным).
Принцип работы:
-
Скрипт выбирает Cookies с именем session у пользователя, при её отсутсвии создает новую сессию и прописывает в Cookies пользователя. Если параметр session существует, то проверяется достоверность ключа, и параметры пользователя если ключ достоверен. Форма приветсвия выдается только если пользователь определен не как гость, в остальных случаях выводится форма авторизации.
-
Во время создания сессии создается запись в таблице базы данных. Если же сессия уже создана, то данные о сессии берутся из базы данных, так же как и имя пользователя (для того что бы внедрить его в приветсвие). Так же обновляется время сессии, данная процедура позволит нам определить количество пользователей за последние 5-10 минут (впрочем, за любой промежуток времени).
Алгоритм скрипта
Код скрипта
#!/usr/bin/perl # Подключаем основные модули use strict; use warnings; use CGI::Cookie; use DBI;
use Storable; use vars '$dbh', '%user_vars'; # Отсылаем заголовок браузеру print "Content-type: text/html; charset=windows-1251\n\n"; # Получаем Cookies пользователя my %cookies = fetch CGI::Cookie; # Определяем IP адрес пользователя (первые две цифры) $user_vars{'remote_addr'} = $ENV{'REMOTE_ADDR'} || 'empty'; $user_vars{'remote_addr'} =~s /^(\d+)\.(\d+)\.\d+\.\d+$/$1\.$2/; $user_vars{'remote_addr'} = 'empty' unless $user_vars{'remote_addr'}; $user_vars{'remote_host'} = $ENV{'REMOTE_HOST'} || 'empty'; $user_vars{'remote_host'} = quotemeta $user_vars{'remote_host'}; $user_vars{'forwarded'} = $ENV{'HTTP_X_FORWARDED_FOR'} || 'empty'; $user_vars{'forwarded'} = quotemeta $user_vars{'forwarded'}; # Подключаемся к базе данных $dbh = 'DBI'->connect('DBI:mysql:database=mybase;host=localhost;port=3306', 'user', 'pass') || die $DBI::errstr; # Проверяем параметр session в Cookies if ($cookies{'session'}) { # Выбираем значение параметра session $cookies{'session'} = $cookies{'session'}->value; $cookies{'session'} =~s /[\W]//g; $cookies{'session'} = 'empty' unless $cookies{'session'}; # Проверяем наличие сессии my $sth = $dbh->prepare('SELECT t1.user, t1.host, t1.ip, t1.forwarded, t2.name FROM session AS t1, users AS t2 WHERE t1.session = \''.$cookies{'session'}.'\' AND t1.user = t2.id LIMIT 1'); $sth->execute(); my $session = $sth->fetchrow_hashref(); $sth->finish(); # Если сессия есть и она не гостевая if ($session && $session > 0 && $$session{'user'} != 4) { #-- Проверяем сессию по IP, хосту и прокси серверу пользователя if ($$session{'ip'} ne $user_vars{'remote_addr'} || $$session{'host'} ne $user_vars{'remote_host'} || $$session{'forvarded'} ne $user_vars{'forvarded'}) { &create_session; &show_authorize_form; } #-- Обновляем время сессии &update_session($cookies{'session'}); #-- Выводим форму приветсвия &show_welcome_form($session); # Если сессия есть и она гостевая } elsif ($session && $session > 0 && $$session{'user'} == 4) { #-- Обновляем время сессии &update_session($cookies{'session'}); #-- Выводим форму авторизации &show_authorize_form; # Если сессии нет } else { #-- Обращаемся к процедуре создания сессии &create_session; #-- Выводим форму авторизации &show_authorize_form; } } else { #-- Обращаемся к процедуре создания сессии &create_session; #-- Выводим форму авторизации &show_authorize_form; } exit;
################################################################################ # Процедура создания сессии sub create_session { # Объявляем переменную новой сессии my $session; # Массив символов для ключа my @rnd_txt = ('0','1','2','3','4','5','6','7','8','9', 'A','a','B','b','C','c','D','d','E','e', 'F','f','G','g','H','h','I','i','J','j', 'K','k','L','l','M','m','N','n','O','o', 'P','p','R','r','S','s','T','t','U','u', 'V','v','W','w','X','x','Y','y','Z','z'); srand; # Генерим ключ for (0..31) { my $s = rand(@rnd_txt); $session .= $rnd_txt[$s] } # Добавляем запись в таблицу сессий $dbh->do('INSERT INTO session SET session = \''.$session.'\', user = 4, time = '.time.', host = \''.$user_vars{'remote_host'}.'\', ip = \''.$user_vars{'remote_addr'}.'\', forwarded = \''.$user_vars{'forwarded'}.'\''); # Определяем код для установки Cookies # В связи с тем, что скрипт внедряется через SSI, то передача Cookies в заголовке никакого # еффекта не произведет, т.к. на странице уже заголовки отправлены и приняты, поэтому Cookies # устанавливаются с помощью JavaScript, иначе же мы просто бы добавили в заголовок строку: # "Set-Cookie: session=$session; expires=Friday, 25-Dec-2020 23:59:59 GMT; path=/; domain=mydomain.ru;\n" # соответсвенно mydomain.ru меняем на свой домен $user_vars{'cookies'} = '<SCRIPT LANGUAGE="JavaScript">this.document.cookie="session='.$session. ';path=/;domain=.mydomain.ru;expires=Sunday,31-Dec-19 23:59:59 GMT;";</SCRIPT> <SCRIPT LANGUAGE="JavaScript">this.document.cookie="session='.$session. ';path=/;domain=www.mydomain.ru;expires=Sunday,31-Dec-19 23:59:59 GMT;";</SCRIPT>'; # Создаем дапм хеша с одним элементом name store {name => 'Guest'}, './data/'.$session; return 1; }
################################################################################ # Процедура обновления сессии sub update_session { my $session = shift; $dbh->do('UPDATE session SET time = '.time.' WHERE session = \''.$session.'\' LIMIT 1'); return 1; }
################################################################################ # Процедура вывода формы авторизации sub show_authorize_form { # Выводим (или не выводим) код установки Cookies print $user_vars{'cookies'} if exists $user_vars{'cookies'}; # Выводим форму авторизации print '<table width=150 border=0> <form action="http://www.mydomain.ru/cgi-bin/auth/login.pl" method=post> <tr><td> Login:<input type="text" name="login" size=10><br> Password:<input type="password" name="pass" size=10> </td></tr> </form> </table>'; exit; }
################################################################################ # Процедура вывода формы приветсвия sub show_welcome_form { my $user = shift; print '<table width=150 border=0> <tr><td> Вы зашли как '.$$user{'name'}.'<br> <a href="http://www.mydomain.ru/cgi-bin/auth/loguot.pl">Выйти</a> </td></tr> </table>'; exit; }
1;
Как видно из кода скрипта, блоки алгоритма к которым обращение идет из разных мест выделены в отдельные процедуры. Отдельно хочу обратить внимание на проверку сессии, кроме ключа я еще проверяю три переменных окружения (REMOTE_ADDR - сокращенный), если соответсвия нет, то просто создается новая сессия для этого пользователя. Я не ввел этот элемент в алгоритм по причине того, что настолько жестко проверять пользователя, не всегда обязательно, а чаще - это лишнее, поэтому эту проверку можно опустить или настроить по своему разумению. На основе ключа можно сразу определить дамп хеша пользователя, где можно собирать
2. Элемент скрипт входа
Данный скрипт делает действие по проверке переданного логина и пароля, после чего, если логин и пароль верны, привязывает уникальный ключ к определенному пользователю, кроме того, в папке data соответсвующий дамп хеша перевести в разряд пользовательских.
Алгоритм скрипта
Код скрипта
#!/usr/bin/perl
# Подключаем основные модули
use strict;
use warnings;
use CGI qw(param);
use CGI::Cookie;
use DBI;
use Storable;
use Digest::MD5 qw(md5_hex);
use vars '$dbh', '%user_vars';
# Получаем Cookies пользователя
my %cookies = fetch CGI::Cookie;
# Если есть ключ сессии
if ($cookies{'session'}) {
# Выбираем значение параметра session
$cookies{'session'} = $cookies{'session'}->value;
$cookies{'session'} =~s /[\W]//g;
unless ($cookies{'session'}) {&error_login};
# Подключаемся к базе данных
$dbh = 'DBI'->connect('DBI:mysql:database=mybase;host=localhost;port=3306', 'user', 'pass')
|| die $DBI::errstr;
# Выбираем данные, переданные пользователем
$user_vars{'login'} = param('login') || &error_login;
$user_vars{'pass'} = param('pass') || &error_login;
# Обрабатывааем данные
$user_vars{'login'} =~s /[\W]//g;
$user_vars{'pass'} = md5_hex($user_vars{'pass'});
$user_vars{'pass'} =~s /[\\\']/\\$1/g;
# Проверяем логин и пароль пользователя
my $sth = $dbh->prepare('SELECT id, name
FROM users
WHERE name = \''.$user_vars{'login'}.'\' AND
pass = \''.$user_vars{'pass'}.'\'
LIMIT 1');
$sth->execute();
my $id = $sth->fetchrow_hashref();
$sth->finish();
# Если соответсвенная запись пользователя найдена
if ($id && $id > 0) {
# Привязываем сессию пользователю
$dbh->do('UPDATE session SET user = '.$$id{'id'}.' WHERE session = \''.$cookies{'session'}.'\' LIMIT 1');
# Согласуем данные пользователя с сессией
my ($session_data, $user_data);
eval ($session_data = retrieve('./data/'.$cookies{'session'}));
eval ($user_data = retrieve('./data/'.$$id{'id'}));
# Если пользовательских данных нет
if ($session_data && !$user_data) {
$user_data->{'name'} = $$id{'name'};
foreach (keys %$session_data) {
next if $_ eq 'name';
$user_data->{$_} = $session_data->{$_}
}
# Если есть данные сессии и пользовательские
} elsif ($session_data && $user_data) {
foreach (keys %$session_data) {
next if $_ eq 'name';
$user_data->{$_} = $session_data->{$_}
}
} else {$user_data->{'name'} = $$id{'name'}}
# Сохраняем данные
store $user_data, './data/'.$$id{'id'};
}
# Производим возврат на исходную страницу или главную страницу сайта
my $referer = $ENV{'REFERER'} || 'http://www.mydomain.ru';
print "Location: $referer\n\n";
exit;
} else {&error_login}
sub error_login {
# Отсылаем заголовок браузеру
print "Content-type: text/html; charset=windows-1251\n\n";
# Выводим форму авторизации
print 'Ошибка!!! Неправильные имя пользователя или пароль!!!' if param('submit');
print '<table width=150 border=0>
<form action="http://www.alfakmv.ru/cgi-bin/auth/login.pl" method=post>
<tr><td>
Login:<input type="text" name="login" size=10><br>
Password:<input type="password" name="pass" size=10>
<input type="submit" name="submit" value="Войти" size=10>
</td></tr>
</form>
</table>';
exit;
}
1;
В вышеуказанном коде я не предусмотрел определение типа ошибки (неправильный логин / неправильный пароль) поэтому, если это все же требуется, то код прийдется усложнять в части выборки информации о пользователе из базы данных: критерием отбора будет являться только поле name, когда же пароль проверяется после выборки.
3. Элемент скрипт выхода
Самый простой скрипт. Он просто переводит уникальный ключ в разряд гостевого.
Код скрипта
#!/usr/bin/perl # Подключаем основные модули use strict; use warnings; use CGI::Cookie; use DBI; use vars '$dbh', '%user_vars'; # Получаем Cookies пользователя my %cookies = fetch CGI::Cookie; # Если есть ключ сессии if ($cookies{'session'}) { # Выбираем значение параметра session $cookies{'session'} = $cookies{'session'}->value; $cookies{'session'} =~s /[\W]//g; # Подключаемся к базе данных $dbh = 'DBI'->connect('DBI:mysql:database=mybase;host=localhost;port=3306', 'user', 'pass') || die $DBI::errstr; # "Отвязываем" сессию от пользователя $dbh->do('UPDATE session SET user = 4 WHERE session = \''.$cookies{'session'}.'\' LIMIT 1'); } # Производим возврат на исходную страницу или главную страницу сайта my $referer = $ENV{'REFERER'} || 'http://www.mydomain.ru'; print "Location: $referer\n\n"; exit;
1;
В этом скрипте не нужны даже лишние проверки, если ключ существует то он станет гостевым. Иначе, если ключа нет, то и авторизации нет, пользователь работает как гость.
|