Краткий туториал по Jelly-Rauth

Просмотров: 3300Комментарии: 0
Web frameworks

Всех тех, кто празднует Рождество Христово 25 декабря -- с Праздником!


Приходят письма с просьбами продемонстрировать особенности работы с моей библиотекой аутентификации для ORM Jelly. С этой целью покажу, как можно написать каркас для системы под следующие требования: админка + регистрация посетителей (то и другое с возможностью автологина).


Итак, одна из возможных архитектур приложения выглядит следующим образом.

Со стороны фронтэнда это Controller_Visitor + унаследованные от него контроллеры для отдельных разделов личного кабинета. Со стороны бэкэнда Controller_Admin, отвечающий за базовые функции типа аутентификации + унаследованные от него контроллеры для отдельных разделов админки. С зарегистрированными посетителями будет работать Model_Visitor, а с администраторами Model_Admin. Такое разделение, на мой взгляд, обеспечивает большую гибкость по сравнению с универсальной моделью Model_User, которая могла бы пользовать тех и других.


Подключаем Jelly-Rauth в application/bootstrap.php, ниже создаем роуты:

// для базового контроллера бэкэнда и обеспечиваемых им функций логина/логаута 
Route::set('admin-login','admin/(<action>(/<params>))', array('action' => 'login|logout', 'params'=>'.*'))
    ->defaults(array(
        'directory' => '',
        'controller' => 'admin',
        'action'     => '',
    )); 
	
// все остальные запросы к бэкэнду перенаправляются дочерним контроллерам, 
// например, Controller_Admin_Dashboard отвечает за отрисовку главной страницы админки и т.п.
Route::set('admin','admin(/<controller>(/<action>(/<params>)))', array('params'=>'.*'))
    ->defaults(array(
        'directory' => 'admin',
        'controller' => 'dashboard',
        'action'     => 'index',
    )); 
	
// логин/логаут/регистрация будут обрабатываться базовым контроллером фронтэнда Controller_Visitor
Route::set('visitor-login','<action>(/<params>)', array('action' => 'login|logout|register', 'params'=>'.*'))
    ->defaults(array(
        'directory' => '',
        'controller' => 'visitor',
        'action'     => '',
    )); 
// остальные группы страниц фронтэнда имеют собственные контроллеры, унаследованные от базового
// можно спорить об удобстве и применимости данной схемы, здесь она с иллюстративной целью
Route::set('visitor', 'visitor(/<controller>(/<action>(/<id>)))')
    ->defaults(array(
        'directory' => 'visitor',
		'controller' => 'dashboard',
		'action' => 'index',
    ));

Правим конфиг модуля jelly-rauth:

return array
(
    // этот блок отвечает за аутентификацию посетителей
	'visitor' => array(
        // название модели без кохановского префикса 'Model_', по умолчанию и если NULL, будет считаться 'user'
        // поскольку нужен автологин, придется создать модель Model_Visitor_Token, наследуемый от Model_Rauth_Token 
        'model_name'    =>  'visitor',
        // способ хеширования, с солью можно и md5, но легкая паранойя не повредит
        'hash_method'   =>  'sha1',
        // соль
        'salt_pattern'  =>  '3, 5, 7, 14, 15, 20, 28',
        // время жизни сессии, в секундах
        'lifetime'      =>  1209600,
        // ключ для хранения данных в сессии
        'session_key'   =>  'rauth_visitor',
        // имя куки для автологина
        'autologin_cookie' =>  'rauthautologin',
        // следует ли проверять наличие и статус персоны в БД при каждом запросе, или довериться сессионным данным
        'strong_check'  =>  FALSE,
        ),    
    'admin' => array(
        // не забыть создать Model_Admin_Token
        'model_name'    =>  'admin', 
        'hash_method'   =>  'sha1',
        // немного изменим паттерн, хотя это похоже на overkill
        'salt_pattern'  =>  '1, 3, 5, 8, 13, 15, 24, 28, 30',
        'lifetime'      =>  1209600,
        // другой ключ сессии, чтобы не перепутать
        'session_key'   =>  'rauth_admin',
        // и другая кука для автологина
        'autologin_cookie' =>  'rauthautologin_admin',
        // что касается, администратора, будем проверять его валидность при каждом действии -- вдруг он уже забанен или удален, хотя в сессии все ОК?
        'strong_check'  =>  TRUE,
        ),
);

Создаем модели (не забывая про модели для токенов, если нужен автологин!):

class Model_Visitor extends Model_Rauth_User {
    public static function initialize(Jelly_Meta $meta)
    {
        $meta->fields = array(
            'address' => new Field_String, // положим, посетитель может заполнить свой адрес, чтобы сервис отсылал ему открытку с поздравлением к Рождеству
        );
        parent::initialize($meta);
    } 
}
class Model_Admin extends Model_Rauth_User {
    public static function initialize(Jelly_Meta $meta)
    {
        $meta->fields = array(
            'phone' => new Field_String, // а от админа нужен телефон, чтобы разбудить его ночью, если что сломалось
        );
        parent::initialize($meta);
    } 
}
class Model_Visitor_Token extends Model_Rauth_Token {
    public static function initialize(Jelly_Meta $meta)
    {
		$meta->fields(array(
            'user' => new Field_BelongsTo(array(
                'foreign'   =>  'visitor',
				// оставляем название столбца без изменений, поскольку оно жестко прописано в коде библиотеки
                'column'    =>  'user_id', 
                )),
            ));
        parent::initialize($meta);
    }
}
class Model_Admin_Token extends Model_Rauth_Token {
    public static function initialize(Jelly_Meta $meta)
    {
		$meta->fields(array(
            'user' => new Field_BelongsTo(array(
                'foreign'   =>  'admin',
				// оставляем название столбца без изменений, поскольку оно жестко прописано в коде библиотеки 
                'column'    =>  'user_id', 
                )),
            ));
        parent::initialize($meta);
    }
}

Создаем необходимиые таблицы в базе данных:

CREATE TABLE IF NOT EXISTS `visitors` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `email` varchar(127) NOT NULL,
  `username` varchar(32) NOT NULL DEFAULT '',
  `password` char(50) NOT NULL,
  `is_active` tinyint(1) NOT NULL DEFAULT '0',
  `logins` int(10) UNSIGNED NOT NULL DEFAULT '0',
  `last_login` int(10) UNSIGNED,
  `address` varchar(512) DEFAULT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `uniq_username` (`username`),
  UNIQUE KEY `uniq_email` (`email`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
CREATE TABLE IF NOT EXISTS `visitor_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 `visitor_tokens`
  ADD CONSTRAINT `visitor_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `visitors` (`id`) ON DELETE CASCADE;
CREATE TABLE IF NOT EXISTS `admins` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `email` varchar(127) NOT NULL,
  `username` varchar(32) NOT NULL DEFAULT '',
  `password` char(50) NOT NULL,
  `is_active` tinyint(1) NOT NULL DEFAULT '0',
  `logins` int(10) UNSIGNED NOT NULL DEFAULT '0',
  `last_login` int(10) UNSIGNED,
  `phone` varchar(32) NOT NULL DEFAULT '',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `uniq_username` (`username`),
  UNIQUE KEY `uniq_email` (`email`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
CREATE TABLE IF NOT EXISTS `admin_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 `admin_tokens`
  ADD CONSTRAINT `admin_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `admins` (`id`) ON DELETE CASCADE;

Далее работаем с контроллером и шаблонами вывода.

Сначала админка:

class Controller_Admin extends Controller_Template {
    
    public $template = 'admin/main';
	// будем хранить наш синглтон аутентификации
    protected $auth;
	// будем хранить данные нашего администратора
    protected $admin;
    
    public function before()
    {
        parent::before();
		// вызываем синглетон соответствующего профиля
        $this->auth = rAuth::instance('admin');
        $this->admin = $this->auth->get_user();
                
        // если персона не залогинилась и не пытается этого сделать (роут не 'login'), редиректим к форме логина
        if (Route::name($this->request->route) != 'admin-login')
        {
            if ( ! $this->admin)
            {
                $this->request->redirect('admin/login');
            }
        } 
    }
    public function action_index()
    {
        $this->template->content = 'hello, admin!';
    }
        
    public function action_login()
    {
        // редирект на главную страницу админки, если уже залогинились
        if ($this->admin)
        {
            Request::instance()->redirect('admin/dashboard');
        }
        
        $this->template->content = View::factory('admin/login')
            ->bind('username', $username)
            ->bind('errors', $errors);
            
        if ($_POST)
        {
            $username = Arr::get($_POST, 'username');
            $password = Arr::get($_POST, 'password');
            $remember = (bool) Arr::get($_POST, 'remember', FALSE);
            
			// проверяем документы
            if ($this->auth->login($username, $password, $remember))
            {
                $this->request->redirect('admin/dashboard');
            } else {
                $errors = array('Incorrect username/password');
            }
        }
    }
    
    public function action_logout()
    {
        $this->auth->logout();
        Request::instance()->redirect('admin/login');
    }
}

Затем аналогично контроллер для посетителей, отличие в наличии регистрации

class Controller_Visitor extends Controller_Template {
    
    public $template = 'visitor/main';
    
    protected $auth;
    
    protected $user;
    
    public function before()
    {
        parent::before();
        $this->auth = rAuth::instance('visitor');
        $this->user = $this->auth->get_user();
                
        if (Route::name($this->request->route) != 'visitor-login')
        {
            if ( ! $this->user)
            {
                $this->request->redirect('login');
            }
        } 
    }
    public function action_index()
    {
        $this->template->content = 'hello, user!';
    }
        
    public function action_login()
    {
        if ($this->user)
        {
            Request::instance()->redirect('visitor/dashboard');
        }
        
        $this->template->content = View::factory('visitor/login')
            ->bind('username', $username)
            ->bind('errors', $errors);
            
        if ($_POST)
        {
            $username = Arr::get($_POST, 'username');
            $password = Arr::get($_POST, 'password');
            $remember = (bool) Arr::get($_POST, 'remember', FALSE);
            
            if ($this->auth->login($username, $password, $remember))
            {
                $this->request->redirect('visitor/dashboard');
            } else {
                $errors = array('Incorrect login/password');
            }
        }
    }
    
    public function action_logout()
    {
        $this->auth->logout();
        
        Request::instance()->redirect('login');
    }
    	
    public function action_register()
    {
        if ($this->user)
        {
            Request::instance()->redirect('visitor/dashboard');
        }
        
        $this->template->content = View::factory('visitor/register')
            ->bind('username', $username)
            ->bind('email', $email)
            ->bind('errors', $errors);
            
        $user = Jelly::factory('visitor');
            
        if ($_POST)
        {
            $username = Arr::get($_POST, 'username', NULL);
            $password = Arr::get($_POST, 'password', NULL);
            $password_confirm = Arr::get($_POST, 'password_confirm', NULL);
            $email = Arr::get($_POST, 'email', NULL);
            
            try
            {
                $user
                    ->set(array(
                        'username' => $username, 
                        'password' => $password, 
                        'password_confirm' => $password_confirm, 
                        'email' => $email, 
                        ))
                    ->save();     
                        
                $this->template->content = View::factory('visitor/register_ok')
                    ->set('user', $user);
                        
            } catch (Validate_Exception $e) {
                $errors = $e->array->errors('validate');
            }
        }
    }
}

Шаблоны приводить не буду, желающие могут посмотреть в архиве 83. Отмечу, что по умолчанию после регистрации залогиниться вам не удастся, поскольку статус is_active при создании нового пользователя устанавливается в FALSE. Как активировать -- решать вам smile


И еще раз -- основные особенности Jelly-Rauth:

1) уменьшение объема кода, раз уж мы используем только Jelly-ORM

2) разделение разных профилей (посетителей и администраторов) на уровне модели и базы данных

3) отстутствие системы ролей, встроенной в модуль, что упрощает код, если такой системы не требуется. Если же она нужна, понадобится модуль ACL с собственной системой ролей.

Обсуждение модуля на форуме Коханы.

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


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

     

  

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

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

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