一.简介:
RBAC(Role-Based Access Control )基于角色的访问控制。
1.基本思想:在用户和访问权限之间引入角色的概念,将用户和角色联系起来,通过对角色的授权来控制用户对系统资源的访问,相对传统访问控制 引入角色极大地简化了权限的管理。
1).角色:可以理解为一定数量的权限的集合,权限的载体。例如:一个论坛系统,“超级管理员”、“版主”都是角色。
2).权限:版主可管理版内的帖子、可管理版内的用户等,这些是权限。
2.至于NIST(The National Institute of Standards and Technology,美国国家标准
与技术研究院)标准RBAC模型的4个部件模型(RBAC0-3)都是基于上面的权限模型图为基础进行的,这里不再一一介绍,有兴趣的朋友可以查找资料详细了解。
二.Yii2中对rbac的实现
1.Yii2 实现了通用的分层的 RBAC,遵循的模型也是NIST RBAC model。
2.在yii2中增加了rule规则的概念,rule是什么鬼呢?举个栗子:对于文章系统而言,我们有管理员和普通用户,允许管理员对文章的任何操作,但是只允许普通用户创建文章和修改自己创建的文章,也就是说普通用户是有修改文章的权限的,但是额加的限制条件是只能修改自己的文章,这个额加的验证工作就是rule规则所要负责的事情。
3.yii2的权限管理实现支持文件和db两个载体,基于db实现方式的核心是四个表:
1)存储角色或权限的表:auth_item (type:1表示 角色;2表示权限)
CREATE TABLE `auth_item` (
`name` varchar(64) NOT NULL,
`type` int(11) NOT NULL,
`description` text,
`rule_name` varchar(64) DEFAULT NULL,
`data` text,
`created_at` int(11) DEFAULT NULL,
`updated_at` int(11) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `rule_name` (`rule_name`),
KEY `type` (`type`),
CONSTRAINT `auth_item_ibfk_1` FOREIGN KEY (`rule_name`) REFERENCES `auth_rule` (`name`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2) 权限和角色的上下级关联表:auth_item_child
(包含关系:角色 可以包含 角色、角色 可以包含 权限、权限 可以包含 权限,但 权限 不可包含 角色)
CREATE TABLE `auth_item_child` (
`parent` varchar(64) NOT NULL,
`child` varchar(64) NOT NULL,
PRIMARY KEY (`parent`,`child`),
KEY `child` (`child`),
CONSTRAINT `auth_item_child_ibfk_1` FOREIGN KEY (`parent`) REFERENCES `auth_item` (`name`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `auth_item_child_ibfk_2` FOREIGN KEY (`child`) REFERENCES `auth_item` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
3)用户与权限(角色)的分配表:auth_assignment
CREATE TABLE `auth_assignment` (
`item_name` varchar(64) NOT NULL,
`user_id` varchar(64) NOT NULL,
`created_at` int(11) DEFAULT NULL,
PRIMARY KEY (`item_name`,`user_id`),
CONSTRAINT `auth_assignment_ibfk_1` FOREIGN KEY (`item_name`) REFERENCES `auth_item` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
3).
4)规则表:auth_rule
CREATE TABLE `auth_rule` (
`name` varchar(64) NOT NULL,
`data` text, //存的是一个序列化的实现了yii\rbac\Rule接口的类的一个对象实例
`created_at` int(11) DEFAULT NULL,
`updated_at` int(11) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `name` (`name`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限规则表';
4.使用 RBAC 涉及到两部分工作。第一部分是建立授权数据, 第二部分是使用这些授权数据在需要的地方执行检查。下面将以第二部分为例进入到Yii2的源码看看具体的用户权限的检查流程(也就是假定表中的数据已经就绪):
下面我们以在基础控制器中的beforeAction中验证权限为例:
public function beforeAction($action)
{
if (!parent::beforeAction($action)) {
return false;
}
$controller = Yii::$app->controller->id;
$action = Yii::$app->controller->action->id;
$permissionName = $controller.'/'.$action; //权限名 (比如上门的post/detail)
if(!\Yii::$app->user->can($permissionName,['article_id'=>Yii::$app->request->get('id')])){ //检查该用户是否有post/detail这个操作的权限
throw new \yii\web\UnauthorizedHttpException('对不起,您现在还没获此操作的权限');
}
return true;
}
追踪user组件的can方法:
public function can($permissionName, $params = [], $allowCaching = true)
{
//缓存中有东东就直接去缓存,使用缓存避免每次都走同样的验证逻辑来提高性能
if ($allowCaching && empty($params) && isset($this->_access[$permissionName])) {
return $this->_access[$permissionName];
}
//这是关键,以DbManager为例进行跟踪分析
$access = $this->getAuthManager()->checkAccess($this->getId(), $permissionName, $params);
if ($allowCaching && empty($params)) {
$this->_access[$permissionName] = $access;
}
return $access;
}
DbManager的checkAcces实现:
/**
* @inheritdoc
*/
public function checkAccess($userId, $permissionName, $params = [])
{
//getAssignments该方法所做的工作就是去用户与权限(角色)的分配表:auth_assignment中找出有关该用户的所有权限和角色,具体实现见下:
$assignments = $this->getAssignments($userId);
$this->loadFromCache();
if ($this->items !== null) {
return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
} else {
return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
}
}
/**
* @inheritdoc
*/
public function getAssignments($userId)
{
if (empty($userId)) {
return [];
}
$query = (new Query)
->from($this->assignmentTable)
->where(['user_id' => (string) $userId]);
$assignments = [];
foreach ($query->all($this->db) as $row) {
$assignments[$row['item_name']] = new Assignment([
'userId' => $row['user_id'],
'roleName' => $row['item_name'],
'createdAt' => $row['created_at'],
]);
}
return $assignments;
}
继续checkAccessRecursive方法:
protected function checkAccessRecursive($user, $itemName, $params, $assignments)
{
//从存储角色或权限的表:auth_item中找出post/detail 这个权限的记录并封装成对象在executeRule方法中使用
if (($item = $this->getItem($itemName)) === null) {
return false;
}
Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
//通过$item->ruleName在规则表:auth_rule中找到该权限(post/detail)对应的rule规则,如果有的话就对该用户执行该权限操作的规则验证,如果验证不通过直接返回false
if (!$this->executeRule($user, $item, $params)) {
return false;
}
//如果在用户与权限(角色)的分配表中匹配到了该权限且规则验证也ok了就说明该用户有权限了呗
if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
return true;
}
$query = new Query;
//如果目前来看该用户没有该操作的权限那么就从权限和角色的上下级关联表中找到该权限的父级,然后递归的检查该用户在该父级里有没有权限,如果父级有权限了那么该权限也有了
$parents = $query->select(['parent'])
->from($this->itemChildTable)
->where(['child' => $itemName])
->column($this->db);
foreach ($parents as $parent) {
if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
return true;
}
}
return false;
}
规则验证方法实现:
/**
* @param string|integer $user 用户 ID.
* @param Item $item 该规则相关的角色或者权限
* @param array $params 传给 ManagerInterface::checkAccess() 的参数
* @return boolean 代表该规则相关的角色或者权限是否被允许
*/
protected function executeRule($user, $item, $params)
{
if ($item->ruleName === null) {
return true;
}
//根据该Item 权限记录对应的rule_name去规则表:auth_rule中找到该条规则记录
//并对该data字段(前面说过存的是序列化的对象)进行反序列化得到yii\rbac\Rule实现类的对象
//然后下面执行该对象的execute方法去验证该用户是否有该权限的使用权
$rule = $this->getRule($item->ruleName);
if ($rule instanceof Rule) {
return $rule->execute($user, $item, $params);
} else {
throw new InvalidConfigException("Rule not found: {$item->ruleName}");
}
}
最后以一个具体例子来讲下rule规则的使用:
正如前面所说的那样,规则是给角色和权限增加额外的约束条件。它是 yii\rbac\Rule 的派生类,它需要实现 yii\rbac\Rule::execute() 方法。在上面那个例子中我们验证当前用户有没有post/detail(假设该操作叫查看文章详情),假设我们只让该文章的创建者有权限查看详情,那么验证规则我们可以这样写:
namespace ziroom\rbac;
use yii\rbac\Rule;
/**
* 检查该文章的创建者id是否和该用户的id一样
*/
class DetailRule extends Rule
{
/**
* @param string|integer $user 用户 ID.
* @param Item $item 该规则相关的角色或者权限
* @param array $params 传给 ManagerInterface::checkAccess() 的参数
* @return boolean 代表该规则相关的角色或者权限是否被允许
*/
public function execute($user, $item, $params)
{
//查找该文章的creator_id
$articleInfo = Article::findOne(['id'=>$params['article_id']]);
return $articleInfo && $articleInfo->creator_id == $user ? true : false;
}
}