Контент


Зарегистрируйтесь!

Добрый день!

Настало время рассказать об одной из главных задач, стоящих перед программистом при создании сайта. Необходимо предусмотреть механизм учета посетителей, включающий регистрацию, аутентификацию, модерирование информации о пользователях… естественно с помощью 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 в шаблоне. Самый простой выход — создать эту переменную в конструкторе этого контроллера, а лучше — переходите-ка вы ко второй части. ;)

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в учебник.

Теги: , , , .


Комментарии (4)

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. Мáкса пишет:

    Вань, спасибо тебе за материал!
    Кстати, в разделе «Пробуем зарегистрироваться» ты приводишь исходный код контроллера, однако не приводишь путь и название файла, под которым его стоит сохранить, для новичков это было бы полезно.
    А после того, как проделал все телодвижения, описанные в статье, получилось следующее:
    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’);»

  2. BIakaVeron пишет:

    Ну, все контроллеры лежат в папке Controllers, имена файлов формируются из имени контроллера без суффикса _Controller (т.е. Base_Controller будет лежать в APPPATH/Controllers/base.php)
    1. Попробуйте открыть файл стилей напрямую через браузер — возможно .htaccess не отрабатывает для существующих файлов, перенаправляя все на фронтенд index.php
    2. А подключен ли модуль Auth? Модель User_Model должна быть в нем (файл Models/user.php)

  3. ABel пишет:

    Спасибо за статью, очень полезная. Теперь, внимание, вопрос:
    Почему в таблице user_tokens в полях created и expires испольуется тип поля int(10), а не date, datetime или timestamp? Что будет, если использовать эти типы данных?
    Я использую PostgreSQL. Но и для MySQL вопрос тоже годится.

  4. BIakaVeron пишет:

    Я бы Вам порекомендовал почитать вот эту статью. Она про MySQL, с PostgreSQL я не работал, но полагаю там примерно то же.



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.