Много разных вариантов реализации ролей доступа мной было испробовано, пока я не нашел идеально подходящий для меня.
Первое, что мне не нравится в RBAC реализованный в Yii2, это возможность использовать несколько ролей. На самом деле, при правильно реализованной иерархической структуре достаточно одной роли.
Второе, что мне не нравится, это хранение назначений отдельно, т.е. связывание роли с пользователем. И это в коробке вообще никак не отключается. Если нужно действительно это отключить, чтобы случайно другой программист не заюзал - нужно переопределять менаджер авторизации и вешать всякие throw и прочее. Роль, имхо, должна указываться в таблице с пользователем, потому что: если нужно делать дамп, то если назначения хранятся в ФС - придется делать дамп и связей; а если назначения хранятся в БД (а хранить их в БД нет никакого смысла вообще), то такое тяжело поддерживать, если вдруг структура ролей будет изменена; и потом, если хранить назначения с ролями отдельно, то у людей получается жуткий говнокод, если нужно иметь возможность "видеть" роль пользователя и менять её динамически. Поэтому, делать нужно правильно изначально: все роли, правила, иерархию наследования храним в ФС, а связываем все это дело, через поле role в таблице с пользователями.
С реализацией все очень просто.
Добавление поля
Во-первых, нужно добавить поле role
в таблицу с пользователями. Лучше всего, чтобы role
имела тип VARCHAR(64) NOT NULL
, но дело ваше. Это не принципиально по сути.
Объявляем роли в модели пользователей
const ROLE_SUPERUSER = 'superuser';
const ROLE_REGISTERED = 'registered';
const ROLE_GUEST = 'guest';
/**
* Возвращает массив всех доступных ролей.
* @return array
*/
static public function roleArray()
{
return [
self::ROLE_SUPERUSER,
self::ROLE_REGISTERED,
self::ROLE_GUEST,
];
}
Настраиваем AuthManager в конфигурации приложения
use app\models\User;
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'itemFile' => '@app/rbac/items.php',
'ruleFile' => '@app/rbac/rules.php',
'assignmentFile' => '@app/rbac/assignments.php', // назначения придется указать, потому что того требуют каноны церкви
'defaultRoles' => User::roleArray(),
],
В defaultRoles
мы указали список всех доступных ролей. Это нужно для того, чтобы менеджер авторизации знал о их существовании. А чтобы он не присваивал их кому попало, нужно реализовать правило на проверку принадлежности к роли у пользователя, который такое поведение будет исключать.
Добавляем правило на проверку принадлежности к роли у пользователя
Теперь нужно создать класс UserRoleRule
в app/rbac
, правило которого будет проверять принадлежность пользователя к роли.
use Yii;
use yii\rbac\Rule;
use app\models\User
class UserRoleRule extends Rule
{
/**
* @inheritdoc
*/
public $name = 'userRole';
private $_assignments = [];
/**
* @inheritdoc
*/
public function execute($user, $item, $params)
{
if ($role = $this->userRole($user)) {
switch ($item->name) {
case User::ROLE_SUPERUSER:
return $role == User::ROLE_SUPERUSER;
case User::ROLE_REGISTERED:
return $role == User::ROLE_SUPERUSER || $role == User::ROLE_REGISTERED;
case User::ROLE_GUEST:
return in_array($role, [User::ROLE_SUPERUSER, User::ROLE_REGISTERED, User::ROLE_GUEST]);
}
}
return false;
}
/**
* @param integer|null $userId ID of user.
* @return string|false
*/
protected function userRole($userId)
{
$user = Yii::$app->user;
if ($userId === null) {
if ($user->isGuest) {
return Users::ROLE_GUEST;
}
return false;
}
if (!isset($this->_assignments[$userId])) {
$role = false;
if (!$user->isGuest && $user->id == $userId) {
$role = $user->role;
} elseif ($user->isGuest || $user->id != $userId) {
$role = User::getRoleOfUser($userId);
}
$this->_assignments[$userId] = $role;
}
return $this->_assignments[$userId];
}
}
Метод userRole
реализует получение роли пользователя даже в случае, если вы проверяете не только текущего пользователя, но и какого-то другого. Этот метод обращается к User::getRoleOfUser()
для получения роли пользователя. Реализация данного метода примерно такая:
/**
* Возвращает роль пользователя по его ID в случае успеха и `false` в случае неудачи.
* @param integer $id ID пользователя.
* @return string|false
*/
static public function getRoleOfUser($id)
{
return (new Query)
->select('role')
->from(self::tableName())
->where(['id' => $id])
->scalar();
}
Также, компонент user
в методе userRole
обращается к свойству role
, которое реализовано через геттер в компоненте yii\web\User
. Вам нужно либо добавить данный метод в уже унаследованный у вас класс yii\web\User
, либо унаследоваться и потом добавить, либо добавить в UserRoleRule
, чего я не рекомендую. Реализация метода примерно такая:
/**
* Возвращает роль пользователя или `null`.
* @return string|null
*/
public function getRole()
{
$identity = $this->getIdentity();
return $identity !== null ? $identity->role : null;
}
Инициализируем RBAC
Т.к. у нас все уже подготовлено, то можно приступить к добавлению ролей, правил и т.д.
В консольном контроллере создаем действие actionInitRbac
:
use Yii;
use app\rbac\UserRoleRule;
use app\models\User;
public function actionInitRbac()
{
$auth = Yii::$app->getAuthManager();
$auth->removeAll();
$userRoleRule = new UserRoleRule;
$auth->add($userRoleRule);
$superuser = $auth->createRole(User::ROLE_SUPERUSER);
$superuser->ruleName = $userRoleRule->name;
$auth->add($superuser);
$registered = $auth->createRole(User::ROLE_REGISTERED);
$registered->ruleName = $userRoleRule->name;
$auth->add($registered);
$guest = $auth->createRole(User::ROLE_GUEST);
$guest->ruleName = $userRoleRule->name;
$auth->add($guest);
$auth->addChild($registered, $guest);
$auth->addChild($superuser, $registered);
}
Иерархия ролей получилась следующей:
- superuser
- | - registered
- - | - guest
Инициализируем
./yii controller-name/init-rbac
Вот и все. Теперь, достаточно указывать в поле role
таблицы пользователя роль пользователя. Проверка прав доступа не изменилась.
Данный метод работает прекрасно без нареканий. Я им полностью доволен.
UPD 26.11.2015 16:26: Добавлена роль гость для общей картины.