Добрый день!
Настало время рассказать об одной из главных задач, стоящих перед программистом при создании сайта. Необходимо предусмотреть механизм учета посетителей, включающий регистрацию, аутентификацию, модерирование информации о пользователях… естественно с помощью Kohana
Как вы могли заметить, к дистрибутиву фреймворка прилагается (если вы конечно поставили галочку в нужном месте при загрузке Kohana) модуль Auth. В нем уже реализованы базовые механизмы аутентификации и авторизации, нам останется только научиться их использовать. Итак, в путь.
Первоначальная настройка
Сам модуль располагается в modules/auth. Сперва необходимо скопировать файл modules/auth/config/auth.php в папку application/config/ (думаю, понятно зачем). Внутри находятся настройки модуля, из которых мы выделим следующие:
$config['driver'] = 'ORM'; $config['hash_method'] = 'sha1'; $config['salt_pattern'] = '1, 3, 5, 9, 14, 15, 20, 21, 28, 30'; |
Сперва указывается используемый драйвер (в поставке есть выбор между ORM и файловым). Насколько я смог понять из исходников, при выборе файлового драйвера пользователи просто хранятся в массиве $config['users'] в виде сочетания ИМЯ=>ХЭШ_ПАРОЛЯ. Поэтому мы будем использовать всю мощь связки ORM+БД.
Алгоритмы хэширования, которые мы можем использовать, возвращает php-функция hash_algos(). Как видите, по умолчанию используется sha1. Честно говоря, не вижу смысла менять его на какой-либо другой, если вы не параноик (либо знаете, что делаете).
Ну, и напоследок — соль. Простое получение хэша от введенного пароля не является безопасным способом, поэтому используют дополнительные куски текста для формирования более стойких хэшей. Суть в том, что перед указанными нами позициями (по умолчанию до первой, третьей и т.д. — и не забываем, что смещение в строке отсчитывается с нуля) вставляются символы, «разбавляющие» хэш самого пароля. В итоге, не зная конкретного расположения «крупинок соли», тяжело вломать хэш. Лучше изменить настройку salt_pattern по умолчанию, но не забывайте, что это надо делать до регистрации пользователей, т.к. меняется алгоритм расчета хэшей. Также помним, что стандартно функция hash() возвращает 40-символьную строку, поэтому указывать смещения 40 и более бессмысленно.
Создаем таблицы в БД
Модуль Auth с драйвером ORM потребует от нас создания нескольких таблиц. К сожалению, в Kohana версии 2.3.1 уже не поставляется демо-контроллер Auth_Demo, в котором, в частности, был приведен текст SQL-запросов для этих самых таблиц. Приведу текст запросов из версии 2.3:
CREATE TABLE IF NOT EXISTS `roles` ( `id` int(11) unsigned NOT NULL auto_increment, `name` varchar(32) NOT NULL, `description` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `roles` (`id`, `name`, `description`) VALUES(1, 'login', 'Login privileges, granted after account confirmation'); INSERT INTO `roles` (`id`, `name`, `description`) VALUES(2, 'admin', 'Administrative user, has access to everything.'); CREATE TABLE IF NOT EXISTS `roles_users` ( `user_id` int(10) unsigned NOT NULL, `role_id` int(10) unsigned NOT NULL, PRIMARY KEY (`user_id`,`role_id`), KEY `fk_role_id` (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `users` ( `id` int(11) unsigned NOT NULL auto_increment, `email` varchar(127) NOT NULL, `username` varchar(32) NOT NULL default '', `password` char(50) NOT NULL, `logins` int(10) unsigned NOT NULL default '0', `last_login` int(10) unsigned, PRIMARY KEY (`id`), UNIQUE KEY `uniq_username` (`username`), UNIQUE KEY `uniq_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `user_tokens` ( `id` int(11) unsigned NOT NULL auto_increment, `user_id` int(11) unsigned NOT NULL, `user_agent` varchar(40) NOT NULL, `token` varchar(32) NOT NULL, `created` int(10) unsigned NOT NULL, `expires` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_token` (`token`), KEY `fk_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `roles_users` ADD CONSTRAINT `roles_users_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, ADD CONSTRAINT `roles_users_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE; ALTER TABLE `user_tokens` ADD CONSTRAINT `user_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; |
Для наделения пользователей определенными правами будут использоваться роли. Сразу создаются две стандартные роли — login (собственно эта роль дает право входить на сайт под этим логином) и admin (тут все понятно). Обратите внимание на имена таблиц и полей, в частности внешних ключей — это одно из требований ORM Kohana (в ближайшем будущем планирую написать статью об этом чуде).
Пробуем зарегистрироваться
Итак, сперва было яйцо… Или курица… В общем, в начале должно быть что-то, от чего нам отталкиваться, и это конечно регистрация пользователя. Давайте накидаем код:
<?php defined('SYSPATH') OR die('No direct access allowed.'); class Auth_Controller extends Template_Controller { public $template = 'index'; public function __construct() { parent::__construct(); $this->template->menus = array ( 'main' => array ( 'title' => 'Главная страница блога', 'link' => '/blog/', ), 'article' => array ( 'title' => 'Статья "Наша первая страница на Kohana"', 'link' => '/2009/01/12/', ), ); $this->template->login = ''; } public function index() { url::redirect(url::base(FALSE).'auth/register'); } public function register() { $this->template->title = 'Регистрация нового пользователя'; $this->template->content = new View('auth/register'); if ($this->input->post()) { // введены данные формы $errors = $data = array( 'email' => '', 'email_confirm' => '', 'username' => '', 'password' => '', 'password_confirm' => '', ); $data = arr::overwrite($data, $_POST); $user = ORM::factory('user'); if (!$user->validate($data)) $errors = $data->errors(); else { $user->add(ORM::factory('role', 'login')); if ($user->save()) url::redirect(url::base(FALSE).'auth/profile'); else { $errors['register'] = 'Ошибка создания пользователя'; } } } else $errors = $data = array(); $this->template->content->errors = $errors; $this->template->content->postdata = is_object($data) ? $data->as_array() : $data; } public function profile() { $this->template->title = 'Профиль пользователя'; $this->template->content = 'Пользователь зарегистрирован!'; } } |
В конструкторе мы заполняем парочку переменных для шаблона index (вспоминаем первую страницу на Kohana). На будущее создаем еще переменную login в шаблоне — мы же захотим, чтобы имя залогиненного пользователя было видно на странице. По умолчанию мы перенаправим пользователя сразу на страницу регистрации (конечно это неправильно, но у нас пока будет так). Обратите внимание на url::base(FALSE) — мы считываем путь к корню сайта, т.к. не всегда сайт располагается в корне.
Самое интересное хранится в методе register. Условие if ($this->input->post()) проверяет наличие данных в глобальном массиве $_POST (наша форма регистрации будет передавать их именно через него, привыкайте к правилу «данные меняются только через POST»). Далее мы инициализируем массивы для хранения ошибок ($errors) и самих данных ($data). Зачем делать именно так, мы видим уже на следующей строке:
$errors = $data = array( 'email' => '', 'email_confirm' => '', 'username' => '', 'password' => '', 'password_confirm' => '', ); $data = arr::overwrite($data, $_POST); |
Если посмотреть в документацию, узнаем, что arr::overwrite() позволяет перезаписать данные из второго параметра в первый, но только по существующим в первом массиве ключам. Т.е. если бы массив $data был пустым, ничего в него бы не записалось. Аналогично с массивом $errors, но его мы заполним позже. Почему именно эти поля? Вспомним заметку в «Напильнике» о доработке объекта Auth_User. Там разбирались правила, назначаемые полям, вводимым пользователем. Соответственно и ошибки могут быть связаны только с этими полями.
Следующий кусок:
$user = ORM::factory('user'); if (!$user->validate($data)) $errors = $data->errors(); else { $user->add(ORM::factory('role', 'login')); if ($user->save()) url::redirect(url::base(FALSE).'auth/profile'); else $errors['register'] = 'Ошибка создания пользователя'; } |
Здесь мы создаем экземпляр модели User (если посмотрите исходники, то увидите, что это расширение модели Auth_User) и проверяем его на валидность, передавая в качестве входных данных массив $data. Если валидация успешна, дополняем данные пользователя ролями (точнее одной ролью ‘login’, она необходима для успешных логинов) и сохраняем в БД. Если произошла ошибка валидации, в массив $errors сохраняем ошибки (они доступны через метод errors(), ну да вы помните). На всякий случай проверяем, сохранилась ли запись в БД, чтобы выдать соответствующую ошибку.
Как вы наверняка заметили, при успешном выполнении сценария идет редирект на метод profile(), в нем мы позже сделаем просмотр подробных данных, но сейчас достаточно и строки об успехе нашего предприятия. Если же произошла ошибка, необходимо не только отобразить ошибки, но и заполнить поля формы ранее введенными пользователем данными (кроме паролей конечно). Для этого используем переменную шаблона $postdata и метод as_array().
Чего-то не хватает…
Конечно, сразу у вас ничего хорошего не запустится, т.к. не созданы шаблоны. Итак, исходники в студию!
Файл application/views/auth/register.php:
<?php defined('SYSPATH') OR die('No direct access allowed.'); ?> <form action='<?=url::base(FALSE)?>auth/register' id='login' method='post'> <fieldset> <legend>Регистрация пользователя</legend> <div id='errors'><?foreach($errors as $key=>$val):?><p><?=$key.":".$val?></p><? endforeach?></div> <ol> <li><label for='email'>e-mail <em>*</em> </label><input type='text' name='email' id='email' size="30" maxlength="32" value="<?=isset($postdata['email']) ? $postdata['email'] : ''?>" /></li> <li><label for='email'>Повторите e-mail <em>*</em> </label><input type='text' name='email_confirm' id='email_confirm' size="30" maxlength="32" value="<?=isset($postdata['email_confirm']) ? $postdata['email_confirm'] : ''?>" /></li> <li><label for='username'>Логин <em>*</em> </label><input type='text' name='username' id='username' size="30" maxlength="32" value="<?=isset($postdata['username']) ? $postdata['username'] : ''?>" /></li> <li><label for='password'>Пароль <em>*</em> </label><input type='password' name='password' id='password' size="30" maxlength="50" /></li> <li><label for='password'>Повторите пароль <em>*</em> </label><input type='password' name='password_confirm' id='password_confirm' size="30" maxlength="50" /></li> <li><input type="submit" value="Зарегистрироваться" /></li> </ol> </fieldset> </form> |
Файл application/views/index.php (он слегка изменился с прошлого раз):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title><?=$title?></title> <link rel="stylesheet" href="<?=url::base(FALSE)?>css/index.css" type="text/css" media="screen" /> </head> <body> <div id='wrapper'> <div id='header'> <h1>Наш блог</h1> <p class='loginboard'><?=$login?></p> </div> <div id='page'> <div id='content'> <?=$content?> </div> <div id='sidebar'> <ul class='menu'> <?foreach ($menus as $menu): ?> <li><a href="<?=$menu['link']?>"><?=$menu['title']?></a></li> <?endforeach;?> </ul> </div> </div> </div> <div id='footer'> Made in Russia, 2009 </div> </body> </html> |
Ну и чтобы форма регистрации смотрелась веселее, добавим немного стилей в css/index.php:
form#login fieldset { margin-bottom: 10px; } form#login legend { padding: 0 2px; font-weight: bold; } form#login label { display: inline-block; font-size: 1.1em; vertical-align: top; width: 200px; color: #A52A2A; } form#login label em { color: red; } form#login ol { margin: 0; padding: 0; } form#login li { list-style: none; padding: 5px; margin: 0; border-bottom: 1px solid silver; } |
Теперь можно открыть http://localhost/auth/ и полюбоваться на форму регистрации. Хотя зачем любоваться, возьмем да зарегистрируемся!
PS. Прощаться не будем
Конечно, даже если все у вас получилось с первого раза, сам по себе результат не очевиден — в факте создания нового пользователя мы можем убедиться только вручную сделав выборку из БД. Поэтому-то я очень скоро напишу вторую часть статьи, в которой мы научимся входить на сайт, выходить и «не терять себя» при переходе с одной страницы на другую. До скорой встречи!
Update
Обратите внимание, что после редактирования шаблона index.php в нем появилась переменная $login, которую мы в конструкторе инициализировали, но ничего конкретного не присваивали. Это как бы шаг в будущее (читайте вторую статью), там мы будем хранить блог авторизации (или приветственное сообщение для авторизованного пользователя).
А вот если вы запустите созданный нами ранее контроллер Blog_Controller (или даже просто страницу по умолчанию http://localhost, если вы перенастроили routing на этот контроллер), то увидите отладочной сообщение об отсутствующей переменной $login в шаблоне. Самый простой выход — создать эту переменную в конструкторе этого контроллера, а лучше — переходите-ка вы ко второй части.
Вань, спасибо тебе за материал!
Кстати, в разделе «Пробуем зарегистрироваться» ты приводишь исходный код контроллера, однако не приводишь путь и название файла, под которым его стоит сохранить, для новичков это было бы полезно.
А после того, как проделал все телодвижения, описанные в статье, получилось следующее:
1. Опять-таки не грузятся стили, даже после использования вот такой конструкции «href=»css/index.css»»
2. Открывается форма регистрации, заполняю, отправляю на сервер, в ответ ошибка: «Fatal error: Class ‘User_Model’ not found in D:\LS\kohana\system\libraries\ORM.php on line 82». Подозреваю, что валится на стркое 41 в контроллере Auth_Controller (application\controllers\auth.php), при вызове «$user = ORM::factory(‘user’);»
Ну, все контроллеры лежат в папке Controllers, имена файлов формируются из имени контроллера без суффикса _Controller (т.е. Base_Controller будет лежать в APPPATH/Controllers/base.php)
1. Попробуйте открыть файл стилей напрямую через браузер — возможно .htaccess не отрабатывает для существующих файлов, перенаправляя все на фронтенд index.php
2. А подключен ли модуль Auth? Модель User_Model должна быть в нем (файл Models/user.php)
Спасибо за статью, очень полезная. Теперь, внимание, вопрос:
Почему в таблице user_tokens в полях created и expires испольуется тип поля int(10), а не date, datetime или timestamp? Что будет, если использовать эти типы данных?
Я использую PostgreSQL. Но и для MySQL вопрос тоже годится.
Я бы Вам порекомендовал почитать вот эту статью. Она про MySQL, с PostgreSQL я не работал, но полагаю там примерно то же.