Сегодня я расскажу вам об очень полезном и удобном компоненте для авторизации пользователей. Как известно в 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 для того, чтобы система не забывала о вас по истечении времени сессии.

