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


не работает. при попытке запретить все действия без авторизации вызывает бесконечный редирект. юзайте SimpleACL
а нет, соврал, работает… ))
ну вот видите ) а с чем была проблема? может стоит её описать, чтобы у других такой не возникло?
да хотел несколько по проще сделать с отношениями 1 к 1, потом назад переставил, и, видимо, что-то забыл… сделал заново, запустилось нормально
Я не пойму, при создании пользователя, программа сохраняет пароль в открытом виде, а программа авторизации проверяет хэш или по методу пользователя. Т.е. в модели users, например в функции beforeSave(), нужно зашифровать пароль тем методом, которым будет проверять Auth. Как это сделать?
Очень полезно!
Спасибо!
Спасибо за замечание.
Существует два способа, если пользоваться 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 для проверки пароля.)
это снова я, всем приветы ))
такой вопрос, переделал все-таки немного под себя – сделал, чтобы у пользователя была только одна группа. при первом открытии страницы, то есть, пока разрешения не занесены в сессию, вылезает ошибка в этой конструкции функции __permitted:
foreach($thisPermissions as $thisPermission){
$permissions[]=$thisPermission['name'];
}
пишет, что, мол, типа Invalid argument supplied for foreach(), но, при распечатке массива $thisPermissions, оказывается, что он вовсе даже не пуст, а вполне даже правильно заполнен.
при чем, при обновлении страницы, варнинг исчезает, разрешения в сессию добавляются корректно и все, в общем, прекрасно. но вот остается только непонятным, почему он вылезает первый раз, и как его исправить…
исправлено добавлением проверки !empty:
if(!empty($thisPermissions)) {
foreach($thisPermissions as $thisPermission) {
$permissions[] = $thisPermission['name'];
}
}
Вы уверены, что в таком решении не возникает ситуации, когда юзер зашёл на страницу, а права = null?
А как ваш контролер app_controller разместить в controllers директории?
Спасибо
просто размести app_controller в controllers директории и все
А как проверить разрешено ли пользователю добавлять сообщения например?
if($this->__permitted('posts','add')){ print 'You can create new post'; } Отчегото не работает..
А как можно сделать так, что-бы авторизация осуществлялась не через mail, а через username?
да, конечно, поле "логин" или "юзернейм" или "имейл" и т.д. можно изменить вот тут:
$this->Auth->fields = array('username'=>'email_address','password'=>'password');
Не совсем понял, где и что прописать, что-бы например разрешить users::listAll, только для группы moderators
в UserControllers поставили var $scaffold; Прикольно так работает. А захотелось внести изменения в login(), а метод пуст. Без скафолда пусто. Не совсем понимаю что происходит в логине. Только авторизация или еще и получение прав? Будьте добры, в двух словах опишите логин.
Как же давно это было :) В общем решение использует компонент Auth (вот тут можно почитать) который, в общем-то, и делает все за нас.
Он сам производит процес входа в систему, в том числе и аутентификации: В beforeFilter вы указываете какие поля использовать для авторизации, а так же при помощи метода allow, вы указываете какие контроллеры и их методы доступны пользователю. В моем примере – эта проверка происходит после логина, и соотвественно ,после получения прав доступа из базы данных.
А не подкажете, почему может возникать ситуация, что когда один пользователь (на счет нескольких не уверен) открывает 2 страницы одновременно (например в две вкладки) и они генерируются одновременно, то в одном из контроллеров появляется форма логина вместо содержимого контроллера?…
Чес тно говоря не встречался с подобной ситуацией.
привет! спасибо за статью
у меня не получается показать авторизован ли пользователь или нет
при вставке такого кода в ctp-файл
echo $this->Auth->user("email_address");
cake пишет:
Undefined property: View::$Auth [APPviewsuserslogin.ctp, line 2]
я только начинаю вникать в cake, поэтому не ругайте сильно, если я что-то не так понял
ведь нужно вставлять этот код в файлы отображения, верно?
ок, я решил свою проблему сам
нашел в документации что извлекать данные о текущей сессии можно так
echo $session->read('Auth.User.username');
вместо username может быть любое поле таблицы users (email_address, id и т.д.)
Добрый день,
прошу прощения, только увидел коментарий. В принципе так можно делать, но вообще, идеалогически, в отображение такие вещи нужно передавать из контроллера. Например,
$this->set("username",$this->Auth->user("username"); а потом в view
echo $username;
Такая ситуация действительно встречается. Для того, чтобы это увидеть – необходимо одну или несколько вкладок в проекте открыть несколько раз, но сделать это необходимо быстро. Также такая ситуация возникает, если 2 раза нажать обновление страницы – мы попадаем на страницу с формой входа.
Даже не знаю с чем это могло быть связанно. Вы пробовали отлаживать? Смотреть сесии и т.д.?