Аутентификация и авторизация
============================

Аутентификация и авторизация необходимы на страницах, доступных лишь некоторым
пользователям. *Аутентификация* — проверка, является ли некто тем, за кого
себя выдаёт. Обычно она подразумевает ввод логина и пароля, но также могут быть
использованы и другие средства, такие как использование смарт-карты, отпечатков
пальцев и др. *Авторизация* — проверка, может ли аутентифицированный пользователь
выполнять определённые действия (их часто обозначают как ресурсы). Чаще всего это
определяется проверкой, назначена ли пользователю определённая роль, имеющая доступ
к ресурсам.

В 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` не
> является пользователем, которого вы проверяете.