Аутентификация и авторизация ============================ Аутентификация и авторизация необходимы на страницах, доступных лишь некоторым пользователям. *Аутентификация* — проверка, является ли некто тем, за кого себя выдаёт. Обычно она подразумевает ввод логина и пароля, но также могут быть использованы и другие средства, такие как использование смарт-карты, отпечатков пальцев и др. *Авторизация* — проверка, может ли аутентифицированный пользователь выполнять определённые действия (их часто обозначают как ресурсы). Чаще всего это определяется проверкой, назначена ли пользователю определённая роль, имеющая доступ к ресурсам. В Yii встроен удобный фреймворк аутентификации и авторизации (auth), который, в случае необходимости, может быть настроен под ваши задачи. Центральным компонентом auth-фреймворка является предопределённый *компонент приложения «user»* — объект, реализующий интерфейс [IWebUser]. Данный компонент содержит постоянную информацию о текущем пользователе. Мы можем получить к ней доступ из любого места приложения, используя `Yii::app()->user`. Используя этот компонент, мы можем проверить, аутентифицирован ли пользователь, используя [CWebUser::isGuest]. Мы можем произвести [вход|CWebUser::login] или [выход|CWebUser::logout]. Для проверки прав на определённые действия удобно воспользоваться [CWebUser::checkAccess]. Также есть возможность получить [уникальный идентификатор|CWebUser::name] и другие постоянные данные пользователя. Определение класса Identity --------------------------- Как было упомянуто ранее, аутентификация — это процесс проверки личности пользователя. Типичное веб-приложение для такой проверки обычно использует логин и пароль. Тем не менее, может потребоваться реализовать проверку другими методами. Чтобы добавить поддержку различных методов аутентификации, в Yii имеется соответствующий identity класс. Мы реализуем класс identity, который содержит нужную нам логику аутентификации. Такой класс должен реализовать интерфейс [IUserIdentity]. Для различных подходов к аутентификации могут быть реализованы различные классы (например, OpenID, LDAP, Twitter OAuth или Facebook Connect). При создании своей реализации необходимо расширить класс [CUserIdentity], являющийся базовым классом, который реализует проверку по логину и паролю. Главная задача при создании класса Identity — реализация метода [IUserIdentity::authenticate]. Данный метод используется для описания основного алгоритма аутентификации. Также данный класс может содержать дополнительную информацию о пользователе, которая необходима нам в процессе работы с его сессией. #### Пример В приведённом ниже примере мы используем класс identity и покажем, как реализовать аутентификацию по базе данных. Данный подход типичен почти для всех приложений. Пользователь будет вводить логин и пароль в форму. Введённые данные будем проверять с использованием модели [ActiveRecord](/doc/guide/database.ar), соответствующей таблице пользователей в БД. В данном примере показано следующее: 1. Реализация метода `authenticate()` для проверки данных по БД. 2. Перекрытие метода `CUserIdentity::getId()` для возврата `_id`. По умолчанию в качестве ID возвращается имя пользователя. 3. Использование метода `setState()` ([CBaseUserIdentity::setState]) для хранения информации, необходимой при каждом запросе. ~~~ [php] class UserIdentity extends CUserIdentity { private $_id; public function authenticate() { $record=User::model()->findByAttributes(array('username'=>$this->username)); if($record===null) $this->errorCode=self::ERROR_USERNAME_INVALID; else if($record->password!==crypt($this->password,$record->password)) $this->errorCode=self::ERROR_PASSWORD_INVALID; else { $this->_id=$record->id; $this->setState('title', $record->title); $this->errorCode=self::ERROR_NONE; } return !$this->errorCode; } public function getId() { return $this->_id; } } ~~~ В следующем подразделе мы рассмотрим реализацию входа и выхода, используя наш identity класс в методе `login` пользователя. Вся информация, которую мы храним в состояниях (путём вызова [CBaseUserIdentity::setState]) будет передана в [CWebUser], который, в свою очередь, будет хранить её в постоянном хранилище, таком как сессии. К данной информации можно будет обращаться как к свойствам [CWebUser]. В нашем примере мы сохранили имя пользователя, используя `$this->setState('title', $record->title);`. Как только пользователь успешно войдёт в приложение, мы сможем получить его `title` используя `Yii::app()->user->title`. > Info|Инфо: По умолчанию [CWebUser] использует сессии для хранения данных. Если вы используете автоматический вход пользователя с помощью cookie ([CWebUser::allowAutoLogin] выставлен в true), данные пользователя будут также сохраняться в cookie. Убедитесь, что эти данные не содержат конфиденциальной информации, такой как пароли. ### Хранение паролей в базе данных Безопасное хранение паролей в базе данных требует дополнительных мер предосторожности. Злоумышленник завладевший таблицей пользователей (или её резервной копией) может восстановить пароли при помощи стандартных методов если вы от них не защищаетесь. В частности, вы должны «солить» пароль перед его хэшированием и использовать стойкую хэш-функцию, результат работы которой мог бы занять у злоумышленника продолжительное время на перебор. Приведенный выше пример использует встроенную PHP-функцию `crypt()`, которая при правильном использовании возвращает достаточно устойчивый хэш. Дополнительные материалы по данной теме: - Стандартная [PHP-функция `crypt()`](http://php.net/manual/ru/function.crypt.php) - Вики-статья [Use crypt() for password storage](http://www.yiiframework.com/wiki/425) Вход и выход ------------ Теперь, когда мы разобрали пример реализации класса identity, мы можем использовать его для реализации входа и выхода: ~~~ [php] // Аутентифицируем пользователя по имени и паролю $identity=new UserIdentity($username,$password); if($identity->authenticate()) Yii::app()->user->login($identity); else echo $identity->errorMessage; … // Выходим Yii::app()->user->logout(); ~~~ Мы создаём новый объект UserIdentity и передаём в его конструктор параметры аутентификации (то есть `$username` и `$password`, введённые пользователем). Далее просто вызываем метод `authenticate()`. В случае успешной проверки данных мы передаём объект в метод [CWebUser::login], который сохраняет информацию в постоянном хранилище (по умолчанию в сессиях PHP) и делает её доступной в последующих запросах. Если аутентификация не проходит, мы можем получить информацию об ошибке из свойства `errorMessage`. Проверить, является ли пользователь аутентифицированным, очень просто. Для этого можно воспользоваться `Yii::app()->user->isGuest`. При использовании постоянного хранилища, такого как сессии (по умолчанию) и/или cookie (описано ниже), для хранения информации о пользователе, пользователь может оставаться аутентифицированным в последующих запросах. В этом случае нет необходимости использовать класс UserIdentity и показывать форму входа. [CWebUser] автоматически загрузит необходимую информацию из постоянного хранилища и использует её при обращении к `Yii::app()->user->isGuest`. Вход на основе cookie --------------------- По умолчанию, после некоторого времени бездействия, зависящего от [настроек сессии](http://php.net/manual/en/session.configuration.php), будет произведён выход из системы. Для того, чтобы этого не происходило, необходимо выставить свойства компонента User [allowAutoLogin|CWebUser::allowAutoLogin] в true и передать необходимое время жизни cookie в метод [CWebUser::login]. Пользователь будет автоматически аутентифицирован на сайте в течение указанного времени даже в том случае, если он закроет браузер. Данная возможность требует поддержки cookie в браузере пользователя. ~~~ [php] // Автоматический вход в течение 7 дней. // allowAutoLogin для компонента user должен быть выставлен в true. Yii::app()->user->login($identity,3600*24*7); ~~~ Как уже упоминалось выше, когда включен вход на основе cookie, состояния, сохраняемые при помощи [CBaseUserIdentity::setState], также будут сохраняться в cookie. При следующем входе состояния считываются из cookie и становятся доступными через `Yii::app()->user`. Несмотря на то, что в Yii имеются средства для предотвращения подмены состояний в cookie на стороне клиента, не рекомендуется хранить в состояниях важную информацию. Гораздо более правильным решением будет хранение её в постоянном хранилище на стороне сервера (например, в БД). Кроме того, для серьёзных приложений рекомендуется улучшить стратегию входа по cookie следующим образом: * При успешном входе после заполнения формы генерируем и храним случайный ключ как в cookie состояния, так и в постоянном хранилище на сервере (т.е. в БД). * При последующих запросах, когда аутентификация производится на основе информации в cookie, мы сравниваем две копии ключа и, перед тем, как аутентифицировать пользователя, проверяем, что они равны. * Если пользователь входит через форму ещё раз, ключ регенерируется. Данная стратегия исключает возможность повторного использования старого состояния cookie, в котором может находится устаревшая информация. Для реализации нужно переопределить два метода: * [CUserIdentity::authenticate()]. Здесь производится аутентификация. Если пользователь аутентифицирован, необходимо сгенерировать новый ключ и сохранить его в cookie состояния (при помощи [CBaseUserIdentity::setState]) и в постояное хранилище на стороне сервера (например, в БД). * [CWebUser::beforeLogin()]. Вызывается перед входом. Необходимо проверить соответствие ключей в состоянии и базе данных. Фильтр контроля доступа ----------------------- Фильтр контроля доступа — схема авторизации, подразумевающая предварительную проверку прав текущего пользователя на вызываемое действие контроллера. Авторизация производится по имени пользователя, IP-адресу и типу запроса. Данный фильтр называется «[accessControl|CController::filterAccessControl]». > Tip|Подсказка: Фильтр контроля доступа достаточен для реализации простых систем. Для более сложных вы можете использовать доступ на основе ролей (RBAC), который будет описан ниже. Для управления доступом к действиям контроллера необходимо переопределить метод [CController::filters] (более подробно описано в разделе [Фильтры](/doc/guide/basics.controller#filter)). ~~~ [php] class PostController extends CController { … public function filters() { return array( 'accessControl', ); } } ~~~ Выше было описано, что фильтр [access control|CController::filterAccessControl] применяется ко всем действиям контроллера `PostController`. Правила доступа, используемые фильтром, определяются переопределением метода [CController::accessRules] контроллера. ~~~ [php] class PostController extends CController { … public function accessRules() { return array( array('deny', 'actions'=>array('create', 'edit'), 'users'=>array('?'), ), array('allow', 'actions'=>array('delete'), 'roles'=>array('admin'), ), array('deny', 'actions'=>array('delete'), 'users'=>array('*'), ), ); } } ~~~ Приведённый код описывает три правила, каждое из которых представлено в виде массива. Первый элемент массива может принимать значения `'allow'` или `'deny'`. Остальные пары ключ-значение задают параметры правила. Правила, заданные выше, можно прочитать следующим образом: действия `create` и `edit` не могут быть выполнены анонимными пользователями, а действие `delete` может быть выполнено только пользователями с ролью `admin`. Правила доступа разбираются поочерёдно в порядке их описания. Первое правило, совпадающее с текущими данными (например, с именем пользователя, ролью или IP) определяет результат авторизации. Если это разрешающее правило, действие может быть выполнено, если запрещающее — не может. Если ни одно из правил не совпало — действие может быть выполнено. > Tip|Подсказка: Чтобы быть уверенным, что действие не будет выполнено, > необходимо запретить все действия, которые не разрешены, определив соответствующее > правило в конце списка: > ~~~ > [php] > return array( > // … разные правила … > // это правило полностью запрещает действие 'delete' > array('deny', > 'actions'=>array('delete'), > ), > ); > ~~~ > Данное правило необходимо, так как если ни одно из правил не совпадёт, > действие продолжит выполнение. Правило доступа может включать параметры, по которым проверяется совпадение: - [actions|CAccessRule::actions]: позволяет указать действия в виде массива их идентификаторов. Сравнение регистронезависимо; - [controllers|CAccessRule::controllers]: позволяет указать контроллеры в виде массива их идентификаторов. Сравнение регистронезависимо. - [users|CAccessRule::users]: позволяет указать пользователей. Для сравнения используется [CWebUser::name]. Сравнение регистронезависимо. В параметре могут быть использованы следующие специальные символы: - `*`: любой пользователь, включая анонимного. - `?`: анонимный пользователь. - `@`: аутентифицированный пользователь. - [roles|CAccessRule::roles]: позволяет указать роли, используя [доступ на основе ролей](/doc/guide/topics.auth#sec5), описанный в следующем разделе. В частном случае, правило применится, если [CWebUser::checkAccess] вернёт true для одной из ролей. Роли стоит использовать в разрешающих правилах так как роль ассоциируется с возможностью выполнения какого-либо действия. Также стоит отметить, что, несмотря на то, что мы используем термин «роль», значением может быть любой элемент auth-фреймворка, такой как роли, задачи или операции; - [ips|CAccessRule::ips]: позволяет указать IP-адрес; - [verbs|CAccessRule::verbs]: позволяет указать тип запросов (например, `GET` или `POST`). Сравнение регистронезависимо; - [expression|CAccessRule::expression]: позволяет указать выражение PHP, вычисление которого будет определять совпадение правила. Внутри выражения доступна переменная `$user`, указывающая на `Yii::app()->user`. Обработка запроса авторизации ----------------------------- При неудачной авторизации, т.е. когда пользователю запрещено выполнять указанное действие, происходит следующее: - Если пользователь не аутентифицирован и в свойстве [loginUrl|CWebUser::loginUrl] компонента user задан URL страницы входа, браузер будет перенаправлен на эту страницу. Заметим, что по умолчанию [loginUrl|CWebUser::loginUrl] перенаправляет к странице `site/login`; - Иначе будет отображена ошибка HTTP с кодом 403. При задании свойства [loginUrl|CWebUser::loginUrl] используется как относительный, так и абсолютный URL. Также можно передать массив, который будет использоваться [CWebApplication::createUrl] при формировании URL. Первый элемент массива задаёт [маршрут](/doc/guide/basics.controller#route) до действия login вашего контроллера, а остальные пары имя-значение — GET-параметры. К примеру, ~~~ [php] array( … 'components'=>array( 'user'=>array( // это значение устанавливается по умолчанию 'loginUrl'=>array('site/login'), ), ), ) ~~~ Если браузер был перенаправлен на страницу входа и вход удачный, вам может понадобиться перенаправить пользователя к той странице, на которой неудачно прошла авторизация. Как же узнать URL той страницы? Мы можем получить эту информацию из свойства [returnUrl|CWebUser::returnUrl] компонента user. Имея её, мы можем сделать перенаправление: ~~~ [php] Yii::app()->request->redirect(Yii::app()->user->returnUrl); ~~~ Контроль доступа на основе ролей -------------------------------- Контроль доступа на основе ролей (RBAC) — простой, но мощный способ централизованного контроля доступа. Для сравнения данного метода с другими обратитесь к [статье в Википедии](http://ru.wikipedia.org/wiki/%D3%EF%F0%E0%E2%EB%E5%ED%E8%E5_%E4%EE%F1%F2%F3%EF%EE%EC_%ED%E0_%EE%F1%ED%EE%E2%E5_%F0%EE%EB%E5%E9). В Yii иерархический RBAC реализован через компонент [authManager|CWebApplication::authManager]. Ниже мы сначала опишем основы данной схемы, затем то, как описывать данные, необходимые для авторизации. В завершение мы покажем, как использовать эти данные для контроля доступа. ### Общие принципы Основным понятием в RBAC Yii является *элемент авторизации*. Элемент авторизации — это права на выполнение какого-либо действия (создать новую запись в блоге, управление пользователями). В зависимости от структуры и цели, элементы авторизации могут быть разделены на *операции*, *задачи* и *роли*. Роль состоит из задач. Задача состоит из операций. Операция — разрешение на какое-либо действие (дальше не делится). К примеру, в системе может быть роль `администратор`, состоящая из задач `управление записями` и `управление пользователями`. Задача `управление пользователями` может состоять из операций `создать пользователя`, `редактировать пользователя` и `удалить пользователя`. Для достижения большей гибкости, роль в Yii может состоять из других ролей и операций. Задача может состоять из других задач. Операция — из других операций. Элемент авторизации однозначно идентифицируется его уникальным именем. Элемент авторизации может быть ассоциирован с *бизнес-правилом* — PHP-кодом, который будет использоваться при проверке доступа. Пользователь получит доступ к элементу только если код вернёт true. К примеру, при определении операции `updatePost`, будет не лишним добавить бизнес-правило, проверяющее соответствие ID пользователя ID автора записи. То есть, доступ к редактированию записи имеет только её автор. Используя элементы авторизации мы можем построить *иерархию авторизации*. Элемент `A` является родителем элемента `B` в иерархии, если `A` состоит из `B` (или `A` наследует права, представленные в `B`). Элемент может иметь несколько потомков и несколько предков. Поэтому иерархия авторизации является скорее частично упорядоченным графом, чем деревом. В ней роли находятся на верхних уровнях, а операции — на нижних. Посередине расположены задачи. После построения иерархии авторизации мы можем назначать роли из неё пользователям нашего приложения. Пользователь получает все права роли, которая ему назначена. К примеру, если назначить пользователю роль `администратор`, он получит административные полномочия, такие как `управление записями` или `управление пользователями` (и соответствующие им операции, такие как `создать пользователя`). А теперь самое приятное. В действии контроллера мы хотим проверить, может ли текущий пользователь удалить определённую запись. При использовании иерархии RBAC и назначенной пользователю роли, это делается очень просто: ~~~ [php] if(Yii::app()->user->checkAccess('deletePost')) { // удаляем запись } ~~~ Настройка менеджера авторизации ------------------------------- Перед тем, как мы перейдём к построению иерархии авторизации и непосредственно проверке доступа, нам потребуется настроить компонент приложения [authManager|CWebApplication::authManager]. В Yii есть два типа менеджеров авторизации: [CPhpAuthManager] и [CDbAuthManager]. Первый использует для хранения данных PHP, второй — базу данных. При настройке [authManager|CWebApplication::authManager] необходимо указать, который из компонентов мы собираемся использовать и указать начальные значения свойств компонента. К примеру, ~~~ [php] return array( 'components'=>array( 'db'=>array( 'class'=>'CDbConnection', 'connectionString'=>'sqlite:path/to/file.db', ), 'authManager'=>array( 'class'=>'CDbAuthManager', 'connectionID'=>'db', ), ), ); ~~~ После этого мы можем обращаться к компоненту [authManager|CWebApplication::authManager] используя `Yii::app()->authManager`. Построение иерархии авторизации ------------------------------- Построение иерархии авторизации состоит из трёх этапов: задания элементов авторизации, описания связей между ними и назначение ролей пользователям. Компонент [authManager|CWebApplication::authManager] предоставляет полный набор API для выполнения поставленных задач. Для определения элемента авторизации следует воспользоваться одним из приведённых ниже методов: - [CAuthManager::createRole] - [CAuthManager::createTask] - [CAuthManager::createOperation] После того, как мы определили набор элементов авторизации, мы можем воспользоваться следующими методами для установки связей: - [CAuthManager::addItemChild] - [CAuthManager::removeItemChild] - [CAuthItem::addChild] - [CAuthItem::removeChild] После этого мы назначаем роли пользователям: - [CAuthManager::assign] - [CAuthManager::revoke] Приведём пример построения иерархии авторизации с использованием данного API: ~~~ [php] $auth=Yii::app()->authManager; $auth->createOperation('createPost','создание записи'); $auth->createOperation('readPost','просмотр записи'); $auth->createOperation('updatePost','редактирование записи'); $auth->createOperation('deletePost','удаление записи'); $bizRule='return Yii::app()->user->id==$params["post"]->authID;'; $task=$auth->createTask('updateOwnPost','редактирование своей записи',$bizRule); $task->addChild('updatePost'); $role=$auth->createRole('reader'); $role->addChild('readPost'); $role=$auth->createRole('author'); $role->addChild('reader'); $role->addChild('createPost'); $role->addChild('updateOwnPost'); $role=$auth->createRole('editor'); $role->addChild('reader'); $role->addChild('updatePost'); $role=$auth->createRole('admin'); $role->addChild('editor'); $role->addChild('author'); $role->addChild('deletePost'); $auth->assign('reader','readerA'); $auth->assign('author','authorB'); $auth->assign('editor','editorC'); $auth->assign('admin','adminD'); ~~~ После создания элементов авторизации, компонент [authManager|CWebApplication::authManager] (или его наследники, например, [CPhpAuthManager], [CDbAuthManager]) загружает их автоматически. То есть, приведённый код запускается один раз, а НЕ для каждого запроса. > Info|Инфо: Довольно громоздкий пример выше предназначен скорее для демонстрации. Разработчикам обычно требуется создать интерфейс администратора и дать возможность пользователям самим построить иерархию авторизации. Использование бизнес-правил --------------------------- При построении иерархии авторизации мы можем назначить роль, задачу или операцию *бизнес-правилу*. Также мы можем указать его при назначении роли пользователю. Бизнес-правило — PHP-код, использующийся при проверке доступа. Возвращаемое данным кодом значение определяет, применять ли данную роль к текущему пользователю. В примере выше мы применили бизнес-правило для описания задачи `updateOwnPost`. В нём мы проверяем, совпадает ли ID текущего пользователя с ID автора записи. Информация о записи в массиве `$params` передаётся разработчиком при проверке доступа. ### Проверка доступа Для проверки доступа нам необходимо знать имя элемента авторизации. К примеру, чтобы проверить, может ли текущий пользователь создать запись, необходимо узнать, имеет ли он права, описанные операцией `createPost`. После этого мы можем вызвать [CWebUser::checkAccess]: ~~~ [php] if(Yii::app()->user->checkAccess('createPost')) { // создаём запись } ~~~ Если правило авторизации использует бизнес-правило, требующее дополнительных параметров, необходимо их передать. К примеру, чтобы проверить, может ли пользователь редактировать запись, мы передаём данные о записи в `$params`: ~~~ [php] $params=array('post'=>$post); if(Yii::app()->user->checkAccess('updateOwnPost',$params)) { // обновляем запись } ~~~ ### Использование ролей по умолчанию Некоторым веб-приложениям требуются очень специфичные роли, которые назначаются каждому или почти каждому пользователю. К примеру, нам необходимо наделить некоторыми правами всех аутентифицированных пользователей. Определять явно и хранить роли для каждого пользователя в этом случае явно неудобно. Для решения этой проблемы можно использовать *роли по умолчанию*. Роль по умолчанию автоматически назначается каждому пользователю. При вызове [CWebUser::checkAccess] сначала проверяются роли по умолчанию. Назначать их явно не требуется. Роли по умолчанию описываются в свойстве [CAuthManager::defaultRoles]. К примеру, приведённая ниже конфигурация описывает две роли по умолчанию: `authenticated` и `admin`. ~~~ [php] return array( 'components'=>array( 'authManager'=>array( 'class'=>'CDbAuthManager', 'defaultRoles'=>array('authenticated', 'admin'), ), ), ); ~~~ Так как роль по умолчанию назначается каждому пользователю, обычно требуется использовать бизнес-правило, определяющее, к каким именно пользователям её применять. К примеру, следующий код определяет две роли: `authenticated` и `admin`, которые соответственно применяются к аутентифицированным пользователям и пользователям с именем `admin`. ~~~ [php] $bizRule='return Yii::app()->user->name === "admin";'; $auth->createRole('admin', 'администратор', $bizRule); $bizRule='return Yii::app()->user->isGuest;'; $auth->createRole('guest', 'гость', $bizRule); ~~~ > Info|Информация: Начиная с версии 1.1.11 массив `$params`, передаваемый в > бизнес-правило, всегда содержит ключ `userId` с id пользователя, для которого проверяется > правило. Это особенно удобно при использовании [CDbAuthManager::checkAccess()] > или [CPhpAuthManager::checkAccess()] когда `Yii::app()->user` не > является пользователем, которого вы проверяете.