Аутентификация с использованием Jelly и Jelly-Auth

Просмотров: 6450Комментарии: 9
Web frameworks

Briefly in English
A short tutorial on using Kohana 3 Jelly and Jelly-Auth modules. English version.


Часть 2



Решил попробовать ORM в лице модного Jelly на примере аутентификации/авторизации, для которой использовал портированный со Sprig модуль Jelly-Auth. До настоящего времени я не работал с ORM подозревая его в тормознутости. Однако выгода быстрого прототипирования, которую он предоставляет, вынуждает попробовать. Скажу, что в реализации аутентификации семейством Auth мне нравится не все (например, хранение данных в сессии). Однако для пробы вполне подойдет.



Сразу хочу предупредить, что код не тестировался на безопасность и ошибкоустойчивость (все это будет позже при наличии времени). Так что в продакшн не выкладывать!


Краткие требования
1. закрытая зона /admin
2. логин/логаут (пока без автологина)
3. создание/редактирование/удаление пользователей из админки (нет открытой регистрации)


Редактируем bootstrap
[code lang="php"]

Kohana::modules(array(

    'database'   => MODPATH.'database',   // Database access

'jelly'      => MODPATH.'jelly',

    'jelly-auth' => MODPATH.'jelly-auth',  // this is jelly-auth plug-in

    'auth'       => MODPATH.'auth',       // Basic authentication

    ));

[/code]
Модули ORM и Auth должны быть подключены, причем модуль Auth д.б. подключен после Jelly-Auth во избежание многих труднопонимаемых глюков.


Пропишем роут админки перед более общим дефолтным
[code lang="php"]

Route::set('admin', 'admin(/<action>(/<param>(/<id>)))')

    ->defaults(array(

        'controller' => 'admin',

        'action'     => 'index',

    ));

[/code]


Создадим контроллер админки, унаследовав его от Controller_Template. Пока нам понадобятся только два свойства
[code lang="php"]

public $template;

protected $auth;

[/code]   

и следующие методы
[code lang="php"]

public function before() (инициализация общих для всех методов свойств контроллера)   

public function action_index() (тестовая страница)

public function action_login() (страница логина)

public function action_users() (управление аккаунтами)

public function action_logout() (выход из админки с редиректом)

protected function _auto_nav() (автогенерация навигационного меню)

[/code]


Создадим базу данных, добавим пользователя БД и запишем это все в конфиг application/config/database.php. Заодно скопируем modules/jelly-auth/config/auth.php в application/.


Создадим таблицы для модуля Jelly-Auth, воспользовавшись включенным в дистрибутив скриптом, и сразу же добавим в базу пользователя-администратора, поскольку открытой регистрации не будет.

[code lang="sql"]

INSERT INTO `users` (`id`, `email`, `username`, `password`, `logins`, `last_login`) VALUES
(1, '', 'admin', 'a3b34d0f297748b8b5741113d9f1ff925d5d7651331a10e0b8', 0, NULL);

INSERT INTO `ko3bricks`.`roles_users` (`user_id`, `role_id`) VALUES ('1', '1'), ('1', '2');

[/code]

Логин 'admin', пароль 'password'


Положим, что для работы с одминкой достаточно иметь роль 'login', роль'admin' пока игнорируем.


Далее работаем с контроллером (шаблоны описывать не буду, все файлы доступны для скачивания)


Метод before()
в нем удобно инициализировать
[code lang="php"]

$this->auth = Auth_Jelly::instance();

[/code]
поскольку пригодится на каждой странице
и заполним
[code lang="php"]

$this->template->menu = $this->_auto_nav();

$this->template->footer = View::factory('admin/footer');

[/code]
в футере у меня будет статистика

В реальном приложении удобно было бы сделать общий менеджер разрешенных/запрещенных для просмотра страниц и поместить его куда-нибудь в before(). Однако в данном случае для краткости изложения обойдемся проверкой в соответствующих методах.


action_login()
Поскольку страница не имеет смысла для залогиненного юзера, проверяем это с учетом наличия у посетителя роли 'login'
[code lang="php"]

if ($this->auth->logged_in('login'))

{

    Request::instance()->redirect('auth/index');

}

[/code]

Обратите внимание: здесь я использовал для проверки метод Jelly_Auth::logged_in($role = NULL), который пытается заавтологинить юзера, если его нет в сессии. На других страницах я использую для проверки метод Auth::get_user(), чтобы автоматически получить данные пользователя. Особенностью Auth::get_user() является отсутствие автоматической проверки роли, то есть ее нужно будет проверять явно. Соответственно, в моем коде наличие роли 'login' проверяется только при логине.


Логин осуществляется после проверки наличия $_POST:

[code lang="php"]

if ($_POST)

{

    $username = $_POST['username'];

    $password = $_POST['password'];

           

    // пока будем танцевать без автологина

    if ($this->auth->login($username, $password, FALSE))

    {

        Request::instance()->redirect('admin/index');

    } else {

        $errors = array('Login or password incorrect');

    }

}

[/code]



action_index()
главная страница сугубо формальная, чтобы продемонстрировать факт успешного логина. Смотрим, залогинен ли посетитель, и если нет -- то редирект на страницу логина

[code lang="php"]

$user = $this->auth->get_user();

if ( ! $user)

{

    Request::instance()->redirect('admin/login');

}

[/code]
полученный объект $user используем для вывода приветствия


action_users($action = NULL, $id = NULL)
это самый "толстый" метод, собственно, в нем-то и можно немного пощупать сам Jelly. Здесь предусмотрим обработку событий

  1. просмотр списка юзеров (метод без параметров)
  2. добавление юзера ($action = 'add')
  3. редактирование данных юзера ($action = 'edit', $id = ID пользователя)
  4. удаление юзера ($action = 'del', $id = ID пользователя)


1) просмотр списка всех юзеров реализуется достаточно просто:
[code lang="php"]

$users = Jelly::select('user')->execute();

[/code]
после чего переменная $users содержит массив найденных объектов.
Модель User у нас описана в модуле Jelly-Auth, поэтому дополнительных телодвижений пока не нужно (потом возможно захочется добавить еще полей, тогда уж придется наследовать от нее свою модель)


2) для добавления юзера извлекаем данные из $_POST (красивее сделать с помощью Arr::extract(), но для наглядности у меня прописано явно). Затем установим значения полей объекта Jelly::factory('user') и сохраним все это:
[code lang="php"]

Jelly::factory('user')

    ->set(array(

        'username' => $username,

        'password' => $password,

        'password_confirm' => $password_confirm,

        'email' => $email,

        'roles' => Jelly::select('role')

            ->where('name', '=', 'login')

            ->execute(),

        ))

    ->save();

[/code]


Заметьте, что мы заполняем и поле роли, выбрав нужные значения из таблицы ролей при помощи Jelly::select().  Посмотрим на выполненные запросы к БД:
[code lang="sql"]

SELECT `roles`.`id` AS `id`, `roles`.`name` AS `name`, `roles`.`description` AS `description` FROM `roles` WHERE `roles`.`name` = 'login'

SELECT COUNT(*) AS `total` FROM `users` WHERE `users`.`username` = 'testtt' ORDER BY `users`.`username` ASC

SELECT COUNT(*) AS `total` FROM `users` WHERE `users`.`email` = 'asdf@vxcvxc.com' ORDER BY `users`.`username` ASC
INSERT INTO `users` (`username`, `password`, `email`, `logins`, `last_login`) VALUES ('testtt', 'ffe4126c4f790dbafca44c82a3793f6cea32feef87efe87cc1', 'asdf@vxcvxc.com', 0, NULL)

INSERT INTO `roles_users` (`user_id`, `role_id`) VALUES (5, 1)

[/code]
Два запроса с COUNT обусловлены требованием уникальности username и email (см. Model_Auth_User из Jelly-Auth), но зачем же COUNT(*), это же неэффективно? Хотя в общем все довольно логично.

Еще одна вещь, которая мне не нравится в Jelly-Auth, это выбрасывание Validation_Exception при ошибке валидации данных. Это простое решение, однако, например, в случае ошибки БД сложнее будет ловить то исключение.  


3) для редактирования юзера я выбираю его из БД по первичному ключу (тогда не нужно писать условие в where())
[code lang="php"]

$user = Jelly::select('user')

    ->load($id);

[/code]


затем проверив $_POST присваиваю нужные поля и сохраняю объект
[code lang="php"]

$user->username = $_POST['username'];

$user->email = $_POST['email'];

                       

if ($_POST['password'])

{

    $user->password = $_POST['password'];

    $user->password_confirm = $_POST['password_confirm'];

}

...

$user->save();

[/code]
Вообще, апдэйтить можно и без предварительной загрузки объекта -- создав его при помощи Jelly::factory('user'), присвоить поля и сохранить $user->save($id). Однако здесь мне нужно вывести поля для редактирования, поэтому без загрузки объекта не обойтись.


4) наконец, удаление юзера осуществляется в два этапа. Сначала запрашивается подтверждение, и только после этого происходит удаление. Причем для предотвращения CSRF пользовательский ID после подтверждения передается через POST (в данном случае явно излишне, но "на вырост" пригодится).
[code lang="php"]

if (isset($_POST['ok']))

{

    if (Jelly::factory('user')->delete($_POST['id']))

    {

        $this->template->content = 'User was deleted';

    } else {

        $this->template->content = 'User was not deleted';

    }

} else {

    Request::instance()->redirect('admin/users');

}

[/code]


action_logout()
тут все просто
[code lang="php"]

$this->auth->logout();

Request::instance()->redirect('admin/login');

[/code]


Как видите, базовые операции реализуются достаточно просто. Однако очевидно, что приведенный вариант админки страдает многими недугами. Это и неразвитость ролевой системы (любой юзер может создавать/редактировать/удалять любого), отсутствие некоторых удобных фич типа автологина и небезопасность использования. Будем дорабатывать.


Скачать / Download 97 (zipped ~6 KiB)

Комментариев: 9 RSS

1 biakaveron 14-04-2010 17:28

Тут на оф.форуме недавно появился код для аналогичных действий (http://forum.kohanaphp.com/comments.php?DiscussionID=5385&page=1#Item_3) smile

Честно говоря, сильно не вглядывался, но что это за защита от CSRF с помощью массива $_POST? Просто отправляется POST-запрос с параметрами id и ok, ведь в этом случае юзер удалится? Обычно генерируется token, который сохраняется в сессии и добавляется в форму в виде hidden-поля. После отправки контроллер сравнивает полученное значение токена и параметра в сессии.

2 Александр Купреев 14-04-2010 20:16

Да, я сегодня утром на форуме увидел, сходные мысли приходят одновременно разным людям smile Но у меня вроде пока побольше функционала, так что статью решил опубликовать.

Насчет CSRF -- уникальный токен это, несомненно, лучшее решение, но в некоторых простых случаях может помочь и POST вместо GET.

3 biakaveron 15-04-2010 11:14

Сейчас никого POST'ом не испугать, к сожалению smile Разве что простого пользователя, который может вбить руками URL для экспериментов, но не более.

4 Sezarin 15-04-2010 12:00

Стандартный модуль ORM, по-моему, здесь ни к чему.

jelly-auth - расширение стандартного модуля auth.

Для работы с jelly - просто изменить в config/auth.php

драйвер на 'driver' => 'Jelly' ;)

5 Александр Купреев 15-04-2010 13:58

biakaveron ну хоть против < img src = "http://site-where-you-are-admin.com/users/delete?filter=all" / > поможет, естественно против аяксовских постов такая медицина бессильна.

Sezarin хм, боюсь, что вы правы, я как-то и не подумал :( проверю, спасибо

6 Kohanetc 16-04-2010 22:21

Крайне "изумлен" некомпетентностью автора. Ч.г. не часто приходится встречаться.

7 Александр Купреев 17-04-2010 11:10

Kohanetc

буду признателен, если вы укажете, в чем тут я некомпетентен. Стараюсь учиться у более опытных людей, поэтому рад указаниям на ошибки.

если неудобно здесь, буду признателен за пояснения по мылу (Alexander (dot) Kupreev (at) gmail (dot) com) или по аське 291688264 или джаббер 291688264@qip.ru. Спасибо

8 Zares 12-05-2010 14:45

Не обращайте внимания, Александр (#comment-226).

Главное - не старайтесь выглядеть экспертом...

Экспертов читать не интересно - их блоги напоминают технические инструкции, зачастую даже вводящие читателя в заблуждение!

Продолжайте писать - у Вас хороший стиль изложения...

9 Александр Купреев 12-05-2010 22:40

Спасибо, стараюсь

Оставьте комментарий!


Используйте нормальные имена.

     

  

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

MaxSiteAuth. Войти через loginza

(обязательно)