user-2-48x48Сегодня я расскажу вам об очень полезном и удобном компоненте для авторизации пользователей. Как известно в CakePHP входит готовый компонент Auth, который, фактически, является не самой понятной частью Cake’а. Благодоря ребятам из Studiocanaria процесс использования этого компонента стал значительно проще, понятней и удобней.

CakePHP Auth Component:

Важной частью стандартного компонента Auth для CakePHP является т.н. ACL - Access Control List. Возможно, он является удобным в тех или иных случаях, но сегодня мы будем говорить о более простой системе прав доступа созданной в Studio Canaria.

Основные черты системы:

  • Авторизация в системе при помощи родного для Cake компонента Auth
  • Создание групп пользователей
  • Использование текстовых описаний правил доступа к контроллерам и действиям вида controller:action, включая использование * для разрешения доступа ко всем подэлементам (т.е.controller:*).
  • Правила доступа кешируются в сессию (таким образом уменьшается количество обращений к базе данных)
  • Вместо простых идентификаторов типа int в системе используются UUID(Universally Unique Identifiers). Это удобно для использования в больших базах данных, в том числе при объединении нескольких баз (впрочем можно использовать и простые int-ключи)

Приступим к делу!

База данных

CREATE TABLE `groups` (
`id` char(36) NOT NULL,
`name` varchar(40) NOT NULL,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `groups_permissions` (
`group_id` char(36) NOT NULL,
`permission_id` char(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `groups_users` (
`group_id` char(36) NOT NULL,
`user_id` char(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `permissions` (
`id` char(36) NOT NULL,
`name` varchar(40) NOT NULL,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `users` (
`id` char(36) NOT NULL,
`email_address` varchar(127) NOT NULL,
`password` varchar(40) NOT NULL,
`active` tinyint(4) NOT NULL default '0',
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Обратите внимание на то, что вместо логина в системе используется електронный адрес – email_address, а так же таблица содержит поле active. Оно указывает может ли пользователь зайти в систему.

Модели

Модель User

app/models/user.php

class User extends AppModel {
    var $displayField = 'email_address';
    var $name = 'User';
    var $validate = array(
        'email_address' => array('email'),
        'password' => array('alphaNumeric'),
        'active' => array('numeric')
    );
    var $hasAndBelongsToMany = array(
            'Group' => array('className' => 'Group',
                        'joinTable' => 'groups_users',
                        'foreignKey' => 'user_id',
                        'associationForeignKey' => 'group_id',
                        'unique' => true
            )
    );
}

Модель Group

app/models/group.php

class Group extends AppModel {
    var $name = 'Group';
    var $hasAndBelongsToMany = array(
            'Permission' => array('className' => 'Permission',
                        'joinTable' => 'groups_permissions',
                        'foreignKey' => 'group_id',
                        'associationForeignKey' => 'permission_id',
                        'unique' => true
            ),
            'User' => array('className' => 'User',
                        'joinTable' => 'groups_users',
                        'foreignKey' => 'group_id',
                        'associationForeignKey' => 'user_id',
                        'unique' => true
            )
    );
}

Модель Permission

app/models/permission.php

class Permission extends AppModel {
    var $name = 'Permission';
    var $hasAndBelongsToMany = array(
            'Group' => array('className' => 'Group',
                        'joinTable' => 'groups_permissions',
                        'foreignKey' => 'permission_id',
                        'associationForeignKey' => 'group_id',
                        'unique' => true
            )
    );
}

Контроллеры

Контроллер Users

app/controllers/users_controller.php

class UsersController extends AppController {
    var $name = 'Users';
    var $scaffold;
    function login(){}
    function logout(){
        $this->Session->del('Permissions');
        $this->redirect($this->Auth->logout());
    }
}

Контроллер Groups

app/controllers/groups_controller.php

class GroupsController extends AppController {
    var $name = 'Groups';
    var $scaffold;
}

Контроллер Permissions

app/controllers/permissions_controller.php

class PermissionsController extends AppController {
    var $name = 'Permissions';
    var $scaffold;
}

Контроллер App

app/app_controller.php

Ниже я постарался перевести коментарии к контроллеру, где содержится основная логика.

class AppController extends Controller {
    /**
     * components
     * 
     * Array of components to load for every controller in the application
     * 
     * @var $components array
     * @access public
     */
    var $components = array('Auth');
    /**
     * beforeFilter
     * 
     * Это событие обрабатывается перед любым действием контроллера
     * 
     * @access public 
     */
    function beforeFilter(){
        //Устанавливаем поля для авторизации в компоненте Auth вместо тех, что идут по-умолчанию
        $this->Auth->fields = array('username'=>'email_address','password'=>'password');
        // Устанавливаем действия доступные без авторизации по всей системе
        $this->Auth->allow(array('display'));//В частности указываем, что на статические страницы /pages/* доступ будет открыт всегда (к примеру, для отображения главной)
        //Страница, на которую будут переходить пользователи после выхода из системы
        $this->Auth->logoutRedirect = '/';
        //Страница, на которую будет переходить пользователь после входа в систему
        $this->Auth->loginRedirect = '/';
        //Расширим компонент Auth при помощи действия isAuthorized
        $this->Auth->authorize = 'controller';
        //Разрешим доступ только тем пользователям чьи профили активны
        $this->Auth->userScope = array('User.active = 1');
        //Передаём компонент авторизации в страницы вида
        $this->set('Auth',$this->Auth->user());
    }
    /**
     * beforeRender
     * 
     * Это событие происходит перед тем, как страница отрисовывается
     *
     * 
     * @access public 
     */
    function beforeRender(){
        //Если пользователь авторизирован, то мы обрабатываем
        //список разрешенных для него действий
        if($this->Auth->user()){
            $controllerList = Configure::listObjects('controller');
            $permittedControllers = array();
            foreach($controllerList as $controllerItem){
                if($controllerItem <> 'App'){
                    if($this->__permitted($controllerItem,'index')){
                        $permittedControllers[] = $controllerItem;
                    }
                }
            }
        }
        $this->set(compact('permittedControllers'));
    }
    /**
     * isAuthorized
     * 
     * Вызывается компонентом Auth для проверки доступа к элементу
     * тут мы и будем проводить нашу проверку
     * 
     * @return true if authorised/false if not authorized
     * @access public
     */
    function isAuthorized(){
        return $this->__permitted($this->name,$this->action);
    }
    /**
     * __permitted
     * 
     * Вспомогательная функция, которая производит проверку прав пользователя
     * описанных в форме $controllerName:$actionName
     * @return 
     * @param $controllerName Object
     * @param $actionName Object
     */
    function __permitted($controllerName,$actionName){
        //Имя контроллеря указываем в нижнем регистре
        $controllerName = low($controllerName);
        $actionName = low($actionName);
        //Если в сессии права не были закешированны
        if(!$this->Session->check('Permissions')){
            //...то подготовим массив для сохранения
            $permissions = array();
            //у всех есть право выйти из системы
            $permissions[]='users:logout';
            //Импортируем модель пользователя, чтобы получить права
            App::import('Model', 'User');
            $thisUser = new User;
            //Получаем текущего пользователя и его группу
            $thisGroups = $thisUser->find(array('User.id'=>$this->Auth->user('id')));
            $thisGroups = $thisGroups['Group'];
            foreach($thisGroups as $thisGroup){
                $thisPermissions = $thisUser->Group->find(array('Group.id'=>$thisGroup['id']));
                $thisPermissions = $thisPermissions['Permission'];
                foreach($thisPermissions as $thisPermission){
                    $permissions[]=$thisPermission['name'];
                }
            }
            //Записываем права в сессию
            $this->Session->write('Permissions',$permissions);
        }else{
            //...видимо права закешированны, загружаем из сессии
            $permissions = $this->Session->read('Permissions');
        }
        //Ищем среди прав соотвествующее текущему
        foreach($permissions as $permission){
            if($permission == '*'){
                return true;//Найдено право СуперАдмина :)
            }
            if($permission == $controllerName.':*'){
                return true;//Разрешаются все действия в данном контроллере
            }
            if($permission == $controllerName.':'.$actionName){
                return true;//Найдено определённое действие
            }
        }
        return false;
    }
}

Вот в общем-то и всё, остались детали.

Страница входа

Чтобы система работала, в неё надо войти.

app/views/users/login.ctp

echo $form->create('User', array('action' => 'login'));
echo $form->input('email_address',array('between'=>'<br>','class'=>'text'));
echo $form->input('password',array('between'=>'<br>','class'=>'text'));
echo $form->end('Sign In');

Настройки системы

Во-первых надо создать первого пользователя и права. Для этого отправляемся в app_controller (/app/app_controller.php) и заменяем строку:
$this->Auth->allow(array(‘display’));
на строку
$this->Auth->allow(array(‘*’));

Предпложим, что ваш проект находится на http://localhost, тогда выполните следущие действия:

  • Переходим на http://localhost/permissions
  • Создаём новое разрешение и в качестве имени указываем *
  • Нажимаем New Group, создаём группу System developers (или на ваш вкус) с правами *
  • Нажимаем на New User, создаём пользователя так, чтобы поле Active было равно 1 (т.е. чекбокс выбран), а группа установленна в System developers

Теперь возвращаем app_controller в оригинальное состояние:
app_controller (/app/app_controller.php) и заменяем строку:
$this->Auth->allow(array(‘*’));
на строку
$this->Auth->allow(array(‘display’));

Вот и все, вы готовы.

Использование системы

Теперь вы можете создать столько разрешений, сколько вам будет угодно, и применить их к целой гамме разных групп на ваш вкус. Это могут быть разрешения вида controller:action, или controller:*

Если у вас будет желание, для неавторизированных пользователей можно добавить сколько угодно доступных действий изменяя app_controller:

function beforeFilter(){
    parent::beforeFilter();
    $this->Auth->allow('display','и','список','любых','других','действий','можно','указать','тут');
}

Таким же образом, можно использовать $permittedControllers для определения разрешённых всегда контроллеров.

Через переменную Auth можно определять авторизирован ли пользователь, а так же его свойства.
К примеру так:

  if ($this->Auth->user() {
     // Пользователь авторизирован
    echo $this->Auth->user("email_address");
  } else {
     echo "Анонимный пользователь";
 }

Обратите внимание на то, что через метод user() можно получать доступ ко всем полям таблицы Users.

Лёгкие подводные камни

Поскольку запись о пользователе кешируется в сессию, иногда возникают проблемы с тем, что её надо обновить. К примеру, когда пользователь меняет свой профиль. После долгих расспросов в Google Groups, выяснилось, что сбросить кеш сессии авторизации можно при помощи следущего кода:

     $user = $this->User->findById($this->Auth->user('id'));
     $this->Session->write($this->Auth->sessionKey, $user['User']);

Вот и всё. Как-нибудь в следующий раз, я расскажу вам об использовании cookies для того, чтобы система не забывала о вас по истечении времени сессии.

This entry was posted on Sunday, February 1st, 2009 and is filed under PHP-разработка, cakephp. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

26 Responses to “Авторизация в CakePHP с пользователями, группами и правилами”

  1. не работает. при попытке запретить все действия без авторизации вызывает бесконечный редирект. юзайте SimpleACL

  2. а нет, соврал, работает… ))

  3. ну вот видите ) а с чем была проблема? может стоит её описать, чтобы у других такой не возникло?

  4. да хотел несколько по проще сделать с отношениями 1 к 1, потом назад переставил, и, видимо, что-то забыл… сделал заново, запустилось нормально

  5. Я не пойму, при создании пользователя, программа сохраняет пароль в открытом виде, а программа авторизации проверяет хэш или по методу пользователя. Т.е. в модели users, например в функции beforeSave(), нужно зашифровать пароль тем методом, которым будет проверять Auth. Как это сделать?

  6. Очень полезно!
    Спасибо!

  7. Спасибо за замечание.
    Существует два способа, если пользоваться Auth-компонентом, то в контроллер надо вставить вызов вида:

    $this->data['User']['password'] = $this->Auth->password($this->data ['User']['password'] , null, true);

    Однако, в модели нельзя использовать компоненты (идеалогически), по этому, в beforeSave стоит добавить следущий код:

    $this->data['password'] = Security::hash($this->data['password'] , null, true);

    (Фактически, это содержание метода password, использует встроенное хеширование, которое так же используется в Auth для проверки пароля.)

  8. это снова я, всем приветы ))
    такой вопрос, переделал все-таки немного под себя – сделал, чтобы у пользователя была только одна группа. при первом открытии страницы, то есть, пока разрешения не занесены в сессию, вылезает ошибка в этой конструкции функции __permitted:

    foreach($thisPermissions as $thisPermission){
    $permissions[]=$thisPermission['name'];
    }

    пишет, что, мол, типа Invalid argument supplied for foreach(), но, при распечатке массива $thisPermissions, оказывается, что он вовсе даже не пуст, а вполне даже правильно заполнен.

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

  9. исправлено добавлением проверки !empty:

    if(!empty($thisPermissions)) {
    foreach($thisPermissions as $thisPermission) {
    $permissions[] = $thisPermission['name'];
    }
    }

  10. Вы уверены, что в таком решении не возникает ситуации, когда юзер зашёл на страницу, а права = null?

  11. А как ваш контролер app_controller разместить в controllers директории?
    Спасибо

  12. просто размести app_controller в controllers директории и все

  13. А как проверить разрешено ли пользователю добавлять сообщения например?
    if($this->__permitted('posts','add')){ print 'You can create new post'; } Отчегото не работает..

  14. А как можно сделать так, что-бы авторизация осуществлялась не через mail, а через username?

  15. да, конечно, поле "логин" или "юзернейм" или "имейл" и т.д. можно изменить вот тут:
    $this->Auth->fields = array('username'=>'email_address','password'=>'password');

  16. Не совсем понял, где и что прописать, что-бы например разрешить users::listAll, только для группы moderators

  17. в UserControllers поставили var $scaffold; Прикольно так работает. А захотелось внести изменения в login(), а метод пуст. Без скафолда пусто. Не совсем понимаю что происходит в логине. Только авторизация или еще и получение прав? Будьте добры, в двух словах опишите логин.

  18. Как же давно это было :) В общем решение использует компонент Auth (вот тут можно почитать) который, в общем-то, и делает все за нас.
    Он сам производит процес входа в систему, в том числе и аутентификации: В beforeFilter вы указываете какие поля использовать для авторизации, а так же при помощи метода allow, вы указываете какие контроллеры и их методы доступны пользователю. В моем примере – эта проверка происходит после логина, и соотвественно ,после получения прав доступа из базы данных.

  19. А не подкажете, почему может возникать ситуация, что когда один пользователь (на счет нескольких не уверен) открывает 2 страницы одновременно (например в две вкладки) и они генерируются одновременно, то в одном из контроллеров появляется форма логина вместо содержимого контроллера?…

  20. Чес тно говоря не встречался с подобной ситуацией.

  21. привет! спасибо за статью

    у меня не получается показать авторизован ли пользователь или нет
    при вставке такого кода в ctp-файл

    echo $this->Auth->user("email_address");

    cake пишет:
    Undefined property: View::$Auth [APPviewsuserslogin.ctp, line 2]

  22. я только начинаю вникать в cake, поэтому не ругайте сильно, если я что-то не так понял

    ведь нужно вставлять этот код в файлы отображения, верно?

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

    echo $session->read('Auth.User.username');

    вместо username может быть любое поле таблицы users (email_address, id и т.д.)

  24. Добрый день,
    прошу прощения, только увидел коментарий. В принципе так можно делать, но вообще, идеалогически, в отображение такие вещи нужно передавать из контроллера. Например,

    $this->set("username",$this->Auth->user("username"); а потом в view

    echo $username;

  25. Такая ситуация действительно встречается. Для того, чтобы это увидеть – необходимо одну или несколько вкладок в проекте открыть несколько раз, но сделать это необходимо быстро. Также такая ситуация возникает, если 2 раза нажать обновление страницы – мы попадаем на страницу с формой входа.

  26. Даже не знаю с чем это могло быть связанно. Вы пробовали отлаживать? Смотреть сесии и т.д.?

Leave a Reply

free counters

Designed by Gabfire
Rambler's Top100