0 follower

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

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

В Yii встроен удобный фреймворк аутентификации и авторизации (auth), который, в случае необходимости, может быть настроен под ваши задачи.

Центральным компонентом auth-фреймворка является предопределённый компонент приложения «user» — объект, реализующий интерфейс IWebUser. Данный компонент содержит постоянную информацию о текущем пользователе. Мы можем получить к ней доступ из любого места приложения, используя Yii::app()->user.

Используя этот компонент, мы можем проверить, аутентифицирован ли пользователь, используя CWebUser::isGuest. Мы можем произвести вход или выход. Для проверки прав на определённые действия удобно воспользоваться CWebUser::checkAccess. Также есть возможность получить уникальный идентификатор и другие постоянные данные пользователя.

1. Определение класса Identity

Как было упомянуто ранее, аутентификация — это процесс проверки личности пользователя. Типичное веб-приложение для такой проверки обычно использует логин и пароль. Тем не менее, может потребоваться реализовать проверку другими методами. Чтобы добавить поддержку различных методов аутентификации, в Yii имеется соответствующий identity класс.

Мы реализуем класс identity, который содержит нужную нам логику аутентификации. Такой класс должен реализовать интерфейс IUserIdentity. Для различных подходов к аутентификации могут быть реализованы различные классы (например, OpenID, LDAP, Twitter OAuth или Facebook Connect). При создании своей реализации необходимо расширить класс CUserIdentity, являющийся базовым классом, который реализует проверку по логину и паролю.

Главная задача при создании класса Identity — реализация метода IUserIdentity::authenticate. Данный метод используется для описания основного алгоритма аутентификации. Также данный класс может содержать дополнительную информацию о пользователе, которая необходима нам в процессе работы с его сессией.

Пример

В приведённом ниже примере мы используем класс identity и покажем, как реализовать аутентификацию по базе данных. Данный подход типичен почти для всех приложений. Пользователь будет вводить логин и пароль в форму. Введённые данные будем проверять с использованием модели ActiveRecord, соответствующей таблице пользователей в БД. В данном примере показано следующее:

  1. Реализация метода authenticate() для проверки данных по БД.
  2. Перекрытие метода CUserIdentity::getId() для возврата _id. По умолчанию в качестве ID возвращается имя пользователя.
  3. Использование метода setState() (CBaseUserIdentity::setState) для хранения информации, необходимой при каждом запросе.
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(!CPasswordHelper::verifyPassword($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.

Инфо: По умолчанию CWebUser использует сессии для хранения данных. Если вы используете автоматический вход пользователя с помощью cookie (CWebUser::allowAutoLogin выставлен в true), данные пользователя будут также сохраняться в cookie. Убедитесь, что эти данные не содержат конфиденциальной информации, такой как пароли.

Хранение паролей в базе данных

Безопасное хранение паролей в базе данных требует определённой аккуратности. Атакующий, получивший доступ к базе или резервным копиям, может восстановить пароли, используя достаточно распространённые приёмы, если от них не защититься. Пример кода выше использует встроенный класс CPasswordHelper, доступный с версии 1.1.14, для хеширования и проверки пароля. CPasswordHelper::hashPassword возвращает стойкий ко взлому хеш.

2. Вход и выход

Теперь, когда мы разобрали пример реализации класса identity, мы можем использовать его для реализации входа и выхода:

// Аутентифицируем пользователя по имени и паролю
$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.

3. Вход на основе cookie

По умолчанию, после некоторого времени бездействия, зависящего от настроек сессии, будет произведён выход из системы. Для того, чтобы этого не происходило, необходимо выставить свойства компонента User allowAutoLogin в true и передать необходимое время жизни cookie в метод CWebUser::login. Пользователь будет автоматически аутентифицирован на сайте в течение указанного времени даже в том случае, если он закроет браузер. Данная возможность требует поддержки cookie в браузере пользователя.

// Автоматический вход в течение 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(). Вызывается перед входом. Необходимо проверить соответствие ключей в состоянии и базе данных.

4. Фильтр контроля доступа

Фильтр контроля доступа — схема авторизации, подразумевающая предварительную проверку прав текущего пользователя на вызываемое действие контроллера. Авторизация производится по имени пользователя, IP-адресу и типу запроса. Данный фильтр называется «accessControl».

Подсказка: Фильтр контроля доступа достаточен для реализации простых систем. Для более сложных вы можете использовать доступ на основе ролей (RBAC), который будет описан ниже.

Для управления доступом к действиям контроллера необходимо переопределить метод CController::filters (более подробно описано в разделе Фильтры).

class PostController extends CController
{
    …
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
}

Выше было описано, что фильтр access control применяется ко всем действиям контроллера PostController. Правила доступа, используемые фильтром, определяются переопределением метода CController::accessRules контроллера.

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) определяет результат авторизации. Если это разрешающее правило, действие может быть выполнено, если запрещающее — не может. Если ни одно из правил не совпало — действие может быть выполнено.

Подсказка: Чтобы быть уверенным, что действие не будет выполнено, необходимо запретить все действия, которые не разрешены, определив соответствующее правило в конце списка:

return array(
    // … разные правила …
    // это правило полностью запрещает действие 'delete'
    array('deny',
        'actions'=>array('delete'),
    ),
);

Данное правило необходимо, так как если ни одно из правил не совпадёт, действие продолжит выполнение.

Правило доступа может включать параметры, по которым проверяется совпадение:

  • actions: позволяет указать действия в виде массива их идентификаторов. Сравнение регистронезависимо;

  • controllers: позволяет указать контроллеры в виде массива их идентификаторов. Сравнение регистронезависимо.

  • users: позволяет указать пользователей. Для сравнения используется CWebUser::name. Сравнение регистронезависимо. В параметре могут быть использованы следующие специальные символы:

    • *: любой пользователь, включая анонимного.
    • ?: анонимный пользователь.
    • @: аутентифицированный пользователь.
  • roles: позволяет указать роли, используя доступ на основе ролей, описанный в следующем разделе. В частном случае, правило применится, если CWebUser::checkAccess вернёт true для одной из ролей. Роли стоит использовать в разрешающих правилах так как роль ассоциируется с возможностью выполнения какого-либо действия. Также стоит отметить, что, несмотря на то, что мы используем термин «роль», значением может быть любой элемент auth-фреймворка, такой как роли, задачи или операции;

  • ips: позволяет указать IP-адрес;

  • verbs: позволяет указать тип запросов (например, GET или POST). Сравнение регистронезависимо;

  • expression: позволяет указать выражение PHP, вычисление которого будет определять совпадение правила. Внутри выражения доступна переменная $user, указывающая на Yii::app()->user.

5. Обработка запроса авторизации

При неудачной авторизации, т.е. когда пользователю запрещено выполнять указанное действие, происходит следующее:

  • Если пользователь не аутентифицирован и в свойстве loginUrl компонента user задан URL страницы входа, браузер будет перенаправлен на эту страницу. Заметим, что по умолчанию loginUrl перенаправляет к странице site/login;

  • Иначе будет отображена ошибка HTTP с кодом 403.

При задании свойства loginUrl используется как относительный, так и абсолютный URL. Также можно передать массив, который будет использоваться CWebApplication::createUrl при формировании URL. Первый элемент массива задаёт маршрут до действия login вашего контроллера, а остальные пары имя-значение — GET-параметры. К примеру,

array(
    …
    'components'=>array(
        'user'=>array(
            // это значение устанавливается по умолчанию
            'loginUrl'=>array('site/login'),
        ),
    ),
)

Если браузер был перенаправлен на страницу входа и вход удачный, вам может понадобиться перенаправить пользователя к той странице, на которой неудачно прошла авторизация. Как же узнать URL той страницы? Мы можем получить эту информацию из свойства returnUrl компонента user. Имея её, мы можем сделать перенаправление:

Yii::app()->request->redirect(Yii::app()->user->returnUrl);

6. Контроль доступа на основе ролей

Контроль доступа на основе ролей (RBAC) — простой, но мощный способ централизованного контроля доступа. Для сравнения данного метода с другими обратитесь к статье в Википедии.

В Yii иерархический RBAC реализован через компонент authManager. Ниже мы сначала опишем основы данной схемы, затем то, как описывать данные, необходимые для авторизации. В завершение мы покажем, как использовать эти данные для контроля доступа.

Общие принципы

Основным понятием в RBAC Yii является элемент авторизации. Элемент авторизации — это права на выполнение какого-либо действия (создать новую запись в блоге, управление пользователями). В зависимости от структуры и цели, элементы авторизации могут быть разделены на операции, задачи и роли. Роль состоит из задач. Задача состоит из операций. Операция — разрешение на какое-либо действие (дальше не делится). К примеру, в системе может быть роль администратор, состоящая из задач управление записями и управление пользователями. Задача управление пользователями может состоять из операций создать пользователя, редактировать пользователя и удалить пользователя. Для достижения большей гибкости, роль в Yii может состоять из других ролей и операций. Задача может состоять из других задач. Операция — из других операций.

Элемент авторизации однозначно идентифицируется его уникальным именем.

Элемент авторизации может быть ассоциирован с бизнес-правилом — PHP-кодом, который будет использоваться при проверке доступа. Пользователь получит доступ к элементу только если код вернёт true. К примеру, при определении операции updatePost, будет не лишним добавить бизнес-правило, проверяющее соответствие ID пользователя ID автора записи. То есть, доступ к редактированию записи имеет только её автор.

Используя элементы авторизации мы можем построить иерархию авторизации. Элемент A является родителем элемента B в иерархии, если A состоит из B (или A наследует права, представленные в B). Элемент может иметь несколько потомков и несколько предков. Поэтому иерархия авторизации является скорее частично упорядоченным графом, чем деревом. В ней роли находятся на верхних уровнях, а операции — на нижних. Посередине расположены задачи.

После построения иерархии авторизации мы можем назначать роли из неё пользователям нашего приложения. Пользователь получает все права роли, которая ему назначена. К примеру, если назначить пользователю роль администратор, он получит административные полномочия, такие как управление записями или управление пользователями (и соответствующие им операции, такие как создать пользователя).

А теперь самое приятное. В действии контроллера мы хотим проверить, может ли текущий пользователь удалить определённую запись. При использовании иерархии RBAC и назначенной пользователю роли, это делается очень просто:

if(Yii::app()->user->checkAccess('deletePost'))
{
    // удаляем запись
}

7. Настройка менеджера авторизации

Перед тем, как мы перейдём к построению иерархии авторизации и непосредственно проверке доступа, нам потребуется настроить компонент приложения authManager. В Yii есть два типа менеджеров авторизации: CPhpAuthManager и CDbAuthManager. Первый использует для хранения данных PHP, второй — базу данных. При настройке authManager необходимо указать, который из компонентов мы собираемся использовать и указать начальные значения свойств компонента. К примеру,

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'sqlite:path/to/file.db',
        ),
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
);

После этого мы можем обращаться к компоненту authManager используя Yii::app()->authManager.

8. Построение иерархии авторизации

Построение иерархии авторизации состоит из трёх этапов: задания элементов авторизации, описания связей между ними и назначение ролей пользователям. Компонент authManager предоставляет полный набор API для выполнения поставленных задач.

Для определения элемента авторизации следует воспользоваться одним из приведённых ниже методов:

После того, как мы определили набор элементов авторизации, мы можем воспользоваться следующими методами для установки связей:

После этого мы назначаем роли пользователям:

Приведём пример построения иерархии авторизации с использованием данного API:

$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 (или его наследники, например, CPhpAuthManager, CDbAuthManager) загружает их автоматически. То есть, приведённый код запускается один раз, а НЕ для каждого запроса.

Инфо: Довольно громоздкий пример выше предназначен скорее для демонстрации. Разработчикам обычно требуется создать интерфейс администратора и дать возможность пользователям самим построить иерархию авторизации.

9. Использование бизнес-правил

При построении иерархии авторизации мы можем назначить роль, задачу или операцию бизнес-правилу. Также мы можем указать его при назначении роли пользователю. Бизнес-правило — PHP-код, использующийся при проверке доступа. Возвращаемое данным кодом значение определяет, применять ли данную роль к текущему пользователю. В примере выше мы применили бизнес-правило для описания задачи updateOwnPost. В нём мы проверяем, совпадает ли ID текущего пользователя с ID автора записи. Информация о записи в массиве $params передаётся разработчиком при проверке доступа.

Проверка доступа

Для проверки доступа нам необходимо знать имя элемента авторизации. К примеру, чтобы проверить, может ли текущий пользователь создать запись, необходимо узнать, имеет ли он права, описанные операцией createPost. После этого мы можем вызвать CWebUser::checkAccess:

if(Yii::app()->user->checkAccess('createPost'))
{
    // создаём запись
}

Если правило авторизации использует бизнес-правило, требующее дополнительных параметров, необходимо их передать. К примеру, чтобы проверить, может ли пользователь редактировать запись, мы передаём данные о записи в $params:

$params=array('post'=>$post);
if(Yii::app()->user->checkAccess('updateOwnPost',$params))
{
    // обновляем запись
}

Использование ролей по умолчанию

Некоторым веб-приложениям требуются очень специфичные роли, которые назначаются каждому или почти каждому пользователю. К примеру, нам необходимо наделить некоторыми правами всех аутентифицированных пользователей. Определять явно и хранить роли для каждого пользователя в этом случае явно неудобно. Для решения этой проблемы можно использовать роли по умолчанию.

Роль по умолчанию автоматически назначается каждому пользователю. При вызове CWebUser::checkAccess сначала проверяются роли по умолчанию. Назначать их явно не требуется.

Роли по умолчанию описываются в свойстве CAuthManager::defaultRoles. К примеру, приведённая ниже конфигурация описывает две роли по умолчанию: authenticated и admin.

return array(
    'components'=>array(
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'defaultRoles'=>array('authenticated', 'admin'),
        ),
    ),
);

Так как роль по умолчанию назначается каждому пользователю, обычно требуется использовать бизнес-правило, определяющее, к каким именно пользователям её применять. К примеру, следующий код определяет две роли: authenticated и admin, которые соответственно применяются к аутентифицированным пользователям и пользователям с именем admin.

$bizRule='return Yii::app()->user->name === "admin";';
$auth->createRole('admin', 'администратор', $bizRule);
 
$bizRule='return Yii::app()->user->isGuest;';
$auth->createRole('guest', 'гость', $bizRule);

Информация: Начиная с версии 1.1.11 массив $params, передаваемый в бизнес-правило, всегда содержит ключ userId с id пользователя, для которого проверяется правило. Это особенно удобно при использовании CDbAuthManager::checkAccess() или CPhpAuthManager::checkAccess() когда Yii::app()->user не является пользователем, которого вы проверяете.

Found a typo or you think this page needs improvement?
Edit it on github !