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 cakephp, PHP-разработка. 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.

34 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. Даже не знаю с чем это могло быть связанно. Вы пробовали отлаживать? Смотреть сесии и т.д.?

  27. Здравствуйте, так и не понял. Вот например есть у меня контроллер опроса. в нем есть действия показать и изменить. Как запретить определенной группе пользователей доступ к конкретному действию?

  28. Сделал все как написано. При переходе по адресу http://www.site.ru/permissions появляется сообщение:
    Not Found
    Error: The requested address '/permissions' was not found on this server.
    Что я сделал не так? Я понимаю что я тупой) Потомучто уже не первую реализацию прав пробую и везде одна и та же ошибка. Что я не делаю? Что нужно еще сделать чтоб это все заработало?

  29. С тем что писал вверху разобрался) Проблема была в том, что нужно было очистить кеш. Но всеравно возникает проблема.
    Undefined property: View::$Auth
    При использовании if ($this->Auth->user())
    Подскажите в чем может быть проблема. Пожалуйста.

  30. Опять я) Незнаю зачем пишу….ведь все равно никто не отвечает) но всетаки. Возникло 2 вопроса:
    1) Добавляю права в таком виде: component1:action1,component2:action2
    Происходит какая-то ерунда,то есть доступа не дается и страница просто обновляется.
    Если добавляю только одно право: component1:action1, то все нормально работает. То есть доступ дается. Как сделать так, чтобы можно было добавлять много разрешений?
    2) Как добавить к этой системе cookies? Это самое важное! Я нигде не могу этого найти. Хотябы ссылку дайте где почитать на английском(

  31. Ребятки. а у меня нет кнопки New Group. Новое разрешение создал, а создание группы нет :(
    чоделоть?

  32. траблу решил переходом на
    http://localhost/groups

  33. Notice (8): Undefined property: PostsController::$Session [APPcontrollersapp_controller.php, line 85]

    Fatal error: Call to a member function check() on a non-object in O:home est232.dyndns.tvwwwappcontrollersapp_controller.php on line 85

    что такое то?:(

  34. Jesus, somebody call ma, really love this blog :-)! But hey bud, I got some bad news, you’re really missing out of a lot of visitors. I’m a blogger myself and I spend a lot of time making and reading them, ones like Авторизация в CakePHP с пользователями, группами и правилами | Meelky Blog. I recently got a new tool and it’s done *wonders* to my entire business, this crazy thing is SO much more powerful and better. It’s only been a week but my revenue has gone up to $258.11 $$CASH$$, all in one day! So want me to do you a favor? Ok, here’s my big secret, where I learned all my tricks: –>>http://doiop.com/TheBlogWealthMakerv2.0 <– Just giving your site a look I can tell you'll probably pull in a couple hundred bucks a month if you just spend a few minutes tweaking some stuff. Damn, I feel like I just made someone's day today. But keep it a secret! I don't want too many people using it. Samuel Awa

Leave a Reply

free counters

Designed by Gabfire
Rambler's Top100