[Yii2笔记]046用户授权(Authorization) [ 技术分享 ]
说明
学习Yii Framework 2(易2框架)的过程是漫长的,也是充满乐趣的,以下是我学习Yii2框架时对官网英文资料(请参见原文网址)的翻译和代码实现,提供了较完整的代码,供你参考。不妥之处,请多多指正!
原文网址:
http://www.yiiframework.com/doc-2.0/guide-security-authorization.html
本文主题:用户授权(Authorization)
用户授权(Authorization)是检查一个用户是否具有某项操作权限的过程,Yii提供了两种授权方法:权限控制过滤器(ACF,Access Control Filter)和基于角色的权限控制(RBAC,Role-Based Access Control)。
1、权限控制过滤器(ACF,Access Control Filter)
如果你的应用仅仅只是简单的权限控制,权限控制过滤器(ACF)将是最佳选择,ACF是一种简单的授权方法,它继承自yii\filters\AccessController。意如其名,权限控制过滤器(ACF)是一个动作过滤器,它可以在控制器或模块中被使用。当一个用户请求执行一个动作时,ACF将检查存取规则列表,然后决定该用户是否允许访问其所请求的动作。
以下代码展示了如何在site控制器中使用ACF:
ACF应用例程
D:\phpwork\basic\controllers\SiteController.php
use yii\web\Controller;
use yii\filters\AccessControl;
class SiteController extends Controller
{
public function behaviors()
{
return [
//access
'access' => [
'class' => AccessControl::className(),
//仅用于'login', 'logout', 'signup',这三个动作
'only' => ['login', 'logout', 'signup'],
//rules
'rules' => [
// 拒绝所有的POST请求
[
'allow' => false,
'verbs' => ['POST']
],
[
//规则为允许访问
'allow' => true,
//应用于两个动作'login', 'signup'
'actions' => ['login', 'signup'],
//应用于所有的guest,以'?'表示
'roles' => ['?'],
],
[
//规则为允许访问
'allow' => true,
//应用于一个动作'logout'
'actions' => ['logout'],
//应用于所有的已登录客户,以'@'表示
'roles' => ['@'],
],
],
],
];
}
public function behaviors()
{
//只要有一条规则可以生效,则此页面都可以访问
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
//此条规则失效,因为下一条规则已约定了所有登录客户都可以访问
'allow' => true,
'actions' => ['index'],
'roles' => ['admin'],//使用'admin',可以是角色(role),也可以是许可(permission)
],
[
//将此条规则删除,则上一条规则可以生效。
'allow' => true,
'roles' => ['@'],
],
],
],
];
}
// ...
}
在上例代码中,ACF作为行为(behavior)被附加到site控制器,这是使用动作过滤器的典型方法。其中,only选项定义了此ACF仅被用于login,logout,signup三个动作。site控制器中的其他动作无需遵守权限控制;rules选项列出了存取规则,解释如下: 1、允许所有访客(未验证身份的用户)可以访问login和signup动作,roles选项包含的问号?(question mark)是一个特殊符号,它代表所有的访客(guest users)。 2、允许所有验证用户访问logout动作,@字符也是一个特殊标记,代表所有的已验证用户(authentication users)。
ACF执行授权检查的方式是:从上到下,逐一检查存取规则,直到它发现有一个规则匹配了当前的执行环境。匹配规则的allow值将用于判断当前用户是否可以授权。如果没有匹配的规则适用,则用户是非授权的,ACF会停止此后的执行。
当ACF判定一个用户不能访问当前动作时,它的后续处理如下: 1、如果一个用户是一个访客,它将调用yii\web\User::loginRequired()将用户浏览器指向登录页面。 2、如果用户已验证过(已登录),它将抛出异常yii\web\ForbiddenHttpException。
你可以配置yii\filters\AccessControl::$denyCallback 属性自定义行为:
[
'class' =>AccessControl::className(),
...
'denyCallback'=>function($rule,$action){
throw new \Exception('You are not allowed to access this page');
}
]
存取规则支持很多选项,以下是所支持选项的汇总,你也可以扩展yii\filters\AccessRule创建自定义的存取规则类。 allow:定义当前规则是允许还是拒绝。其值是字符串,取值范围:'allow','deny' actions:定义匹配哪个动作。其值是索引数组,每个数组元素对应一个动作id,字母区分大小,如果此选项为空或没有设置,意味着此规则适用于所有动作。 controllers:定义匹配哪个控制器。其值是索引数组,每个数组元素对应一个控制器id,如果有模块,则需要在控制器id前添加模块id,字母区分大小。如果此选项为空或没有设置,意味着此规则适用于所有控制器。 //roles//ACF//Route roles:定义此规则匹配哪个用户角色。有两个特殊角色可以被识别,通过yii\web\User::$isGuest 可以检测出来。
?:匹配一个访客(未验证用户)。
@:匹配一个已验证用户。
使用其他角色名称会触发调用yii\web\User::can(),它将需要使用RBAC(下节将详述),如果此选项为空或没有设置,意味着此规则适用于所有角色。
ips:定义客户端允许使用的IP地址。IP地址后部可以使用通配符,可以代表相同前段的多个IP地址,例如:'192.168.'匹配'192.168.'网段的所有IP地址。如果此选项为空或没有设置,意味着此规则适用于所有IP地址。 verbs:定义规则匹配的请求方法(如GET、POST),字母区分大小。 matchCallback:定义一个PHP回调函数,由它来决定本条规则是否适用。 denyCallback:定义一个PHP回调函数,由它来决定本条是否拒绝。
下例演示了matchCallback选项的用法,这样你就可以自定义存取检查逻辑了:
use yii\filters\AccessControl;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['special-callback'],
'rules' => [
[
'actions' => ['special-callback'],
'allow' => true,
'matchCallback' => function ($rule, $action) {
return date('d-m') === '31-10';
}
],
],
],
];
}
//调用Match callback,这个页面只有在10月31日时会被调用。
public function actionSpecialCallback()
{
return $this->render('happy-halloween');
}
}
2、基于角色的存取控制(RBAC,Role Based Access Control)
1、Yii2实现了NIST RBAC模型的RBAC2级别的通用等级权限控制(General Hierarchical RBAC) 2、Yii2实现了角色的多级分层 3、Yii2实现了许可的多级分层
基于角色的权限控制(RBAC)提供了一个简单而强大的权限控制。关于RBAC与其他商业权限控制模型的区别请参考Wikipedia: https://en.wikipedia.org/wiki/Role-based_access_control
Yii依据NIST RBAC模型实现了一个通用等级权限控制(General Hierarchical RBAC,还有一种权限控制叫Restricted Hierarchical RBAC) ,使用authManager应用组件就可以实现RBAC的功能。
NIST RBAC模型文档:
http://csrc.nist.gov/rbac/sandhu-ferraiolo-kuhn-00.pdf
authManager文档:
http://www.yiiframework.com/doc-2.0/yii-rbac-managerinterface.html
DbManager文档:
http://www.yiiframework.com/doc-2.0/yii-rbac-dbmanager.html
PhpManager文档:
http://www.yiiframework.com/doc-2.0/yii-rbac-phpmanager.html
使用RBAC包括两项工作: 1、构建RBAC授权(authorization)数据(authorization data) 2、在需要的地方使用授权数据执行存取检查(access check)
为了后面更好的理解,我们首先来介绍一些RBAC的基本概念:
基本概念(Basic Concepts)
permission,许可,层级,type=2(表auth_item) rule,规则 role,角色,层级,type=1(表auth_item) user,用户 许可(permission)是最基本的权限,如'创建文章'(creating posts)、'更新文章'(updating posts)等,一个角色(role)是多个许可的集合。一个角色可以分配给一个或多个用户,如果要检查一个用户是否拥有特定的许可,我们可以检查这个用户是否分配到了一个包含此许可的角色。 与一个角色或许可相关联,可以定义一个规则(rule),一个规则就是一段代码,当判断一个用户是否拥有一个角色或许可时,这段代码就会被执行。例如:"update post"许可可以通过一个规则去检测当前用户是否是post的创建者,通过存取检测,如果用户不是post的创建者,他将没有"update post"许可。 角色和许可都可以分层,一个角色可以包含其他角色或许可,一个许可可以包含其他许可。Yii实现了偏序层次结构(partial order hierarchy),包含了很多特殊的树形层级,一个角色可以包含一个许可,反之变然(it is not true vice versa)。
配置RBAC(Configuring RBAC)
在创建权限数据(authorization data)、执行权限检测(access checking)之前,我们需要先配置authManager应用组件。Yii提供了两种权限管理器(authorization manager):yii\rbac\PhpManager和yii\rbac\DbManager。前者使用PHP脚本文件存储权限数据,后者使用数据库存储权限数据。如果你的应用并没有太多的动态角色和许可管理,可以考虑使用前者。
使用PhpManager
在应用配置中配置authManager使用yii\rbac\PhpManager
return [
......
'components'=>[
'authManager'=>[
'class'=>'yii\rbac\PhpManager',
],
],
];
配置好后,authManager就可以通过\Yii::$app->authManager来获取了。 默认情况下,yii\rbac\PhpManager存储RBAC数据在@app/rbac目录下,应确保该目录及其下的文件是可以被Web服务器进程写入的,以保障权限结构(permissions hierarchy)在线变动时可以被修改。
使用DbManager
return [
//....
'components'=>[
'authManager'=>[
'class'=>'yii\rbac\DbManager',
],
//.....
],
];
注意:如果你使用的是yii2-basic-app模板,需要在config/console.php和config/web.php文件中都配置authManager;yii2-advanced-app只需在common/config/main.php文件中配置一次即可。
DbManager使用四个数据表来存储数据: 1、itemTable,用于存储权限项(authorization item),包括角色(type=1)和许可(type=2),默认表名是"auth_item" 2、itemChildTable,用于存储权限项的层次结构(authorization item hierarchy),默认表名是"auth_item_child" 3、assignmentTable,用于存储权限项分配(authorization item assignment),默认表名是"auth_assignment" 4、ruleTable,用于存储规则(rule),默认表名是"auth_rule"
在继续之前,需要在数据库中提前创建这些表,要实现此目的,你可以使用存储在@yii/rbac/migrations迁移项(migration):
yii migrate --migrationPath=@yii/rbac/migrations
关于不同命名空间中实现迁移请阅读Separated Migration: http://www.yiiframework.com/doc-2.0/guide-db-migrations.html#separated-migrations
MariaDB创建的4张表:
/*
-- ----------------------------
-- Table structure for auth_assignment
-- ----------------------------
CREATE TABLE `auth_assignment` (
`item_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`user_id` varchar(64) COLLATE utf8_unicode_ci 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 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Table structure for auth_item
-- ----------------------------
CREATE TABLE `auth_item` (
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`type` smallint(6) NOT NULL,
`description` text COLLATE utf8_unicode_ci,
`rule_name` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
`data` blob,
`created_at` int(11) DEFAULT NULL,
`updated_at` int(11) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `rule_name` (`rule_name`),
KEY `idx-auth_item-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 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Table structure for auth_item_child
-- ----------------------------
CREATE TABLE `auth_item_child` (
`parent` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`child` varchar(64) COLLATE utf8_unicode_ci 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 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Table structure for auth_rule
-- ----------------------------
CREATE TABLE `auth_rule` (
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`data` blob,
`created_at` int(11) DEFAULT NULL,
`updated_at` int(11) DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
*/
现在就可以使用\Yii::$app->authManager来获取authManger了。
创建权限数据(Building Authorization Data)
创建权限数据需要完成以下任务: 1、定义角色(role)和许可(permission) 2、建立角色和许可之间的关系 3、定义规则(rule) 4、将规则与角色和许可相关联 5、分配角色到用户
根据许可灵活性需求(permissions flexibility requirements),以上任务可以通过不同的方法去实现。
如果你的许可层级(permissions hierarchy)基本就不会更改,并且用户数量也是固定的,你可以通过authManager提供的API使用控制台命令来初始化授权数据。
<?php
namespace app\commands;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$auth = Yii::$app->authManager;
//添加"createPost"许可//createPost//post
$createPost = $auth->createPermission('createPost');
$createPost->description = 'Create a post';
$auth->add($createPost);
//添加"updatePost"许可
$updatePost = $auth->createPermission('updatePost');
$updatePost->description = 'Update post';
$auth->add($updatePost);
//添加"author"角色,并给它分配"createPost"许可
$author = $auth->createRole('author');
$auth->add($author);
$auth->addChild($author, $createPost);
//添加"admin"角色,并给它分配置"updatePost"许可,将"admin"授予"author"相同的权限
$admin = $auth->createRole('admin');
$auth->add($admin);
$auth->addChild($admin, $updatePost);
$auth->addChild($admin, $author);
//分配角色到用户,1和2是用户id,由IdentityInterface::getId()获得,通常在User模型中完成。
$auth->assign($author, 2);
$auth->assign($admin, 1);
}
}
注意:如果你使用的是advanced模板,你需要将RbacController放在console/controllers目录下,并将命名空间修改为console/controllers。
执行yii rbac/init命令前,需要做两件事:
//1、composer安装"letyii/yii2-rbac-mongodb"
"letyii/yii2-rbac-mongodb": "dev-master",
//2、配置console的mogodb: D:\phpwork\advanced\console\config\main.php
'components' => [
'authManager' => [
'class' => 'letyii\rbacmongodb\MongodbManager',//MongoDB中使用,在console中也需要单独配置
// 'class' => 'yii\rbac\DbManager',//MariaDB使用
],
],
//执行yii rbac/init命令
D:\phpwork\advanced>yii rbac/init
D:\phpwork\advanced> 在MongoDB数据库中创建了上述的四个集合,并在这些集合中写入了相应的关系记录,OK!
//在MariaDB中插入了以下数据:
/*
//auth_item
INSERT INTO `auth_item` VALUES ('admin', '1', null, null, null, '1493010703', '1493010703');
INSERT INTO `auth_item` VALUES ('author', '1', null, null, null, '1493010703', '1493010703');
INSERT INTO `auth_item` VALUES ('createPost', '2', 'Create a post', null, null, '1493010703', '1493010703');
INSERT INTO `auth_item` VALUES ('updatePost', '2', 'Update post', null, null, '1493010703', '1493010703');
//auth_item_child
INSERT INTO `auth_item_child` VALUES ('admin', 'author');
INSERT INTO `auth_item_child` VALUES ('admin', 'updatePost');
INSERT INTO `auth_item_child` VALUES ('author', 'createPost');
//auth_assignment
INSERT INTO `auth_assignment` VALUES ('admin', '1', '1493010993');
INSERT INTO `auth_assignment` VALUES ('author', '2', '1493010993');
*/
通过执行yii rbac/init命令,我们可以获取以下的层级结构:
许可->角色->用户(由低到高)
updatePost->admin->(Jane,ID=1)
createPost->author->(John,ID=2)
author->admin
作者可以创建文章,admin可以更新文章和作者可以做的任何事情。
如果你的应用允许用户注册,需要为这些新用户分配这些角色,例如:为了让每一位注册用户成为作者,在高级模板中,你需要修改frontend\models\SignupForm::signup(),代码如下:
public function signup()
{
if ($this->validate()) {
$user = new User();
$user->username = $this->username;
$user->email = $this->email;
$user->setPassword($this->password);
$user->generateAuthKey();
$user->save(false);
// the following three lines were added:
$auth = Yii::$app->authManager;
$authorRole = $auth->getRole('author');
$auth->assign($authorRole, $user->getId());
return $user;
}
return null;
}
对于复杂权限控制的应用,需要动态更新权限数据,可能需要定义用户界面(如:admin面板),在其中使用authManager的API进行相关的开发。
//检测当前用户是否具有author许可权限
if(Yii::$app->getUser()->can('author')){
echo "<br>author";
}else{
echo "<br>NO";
}
//使用规则(Using Rules) 如前所述,规则为角色和许可添加额外的约束。每个规则都是yii\rbac\Rule的子类,它必须实现execute()方法,在前面创建的层级结构author(作者)不能编辑他自己发表的文章,我们现在来修复这个问题,首先我们需要一个规则去验证用户是文章的作者:
<?php
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if authorID matches user passed via params
*/
class AuthorRule extends Rule
{
public $name = 'isAuthor';
/**
* @param string|int $user the user ID.
* @param Item $item the role or permission that this rule is associated with
* @param array $params parameters passed to ManagerInterface::checkAccess().
* @return bool a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
return isset($params['post']) ? $params['post']->createdBy == $user : false;
}
}
上面的规则检测此文章是否是由$user 创建的,我们将使用此前使用过的命令创建一个特殊的许可updateOwnPost:
$auth = Yii::$app->authManager;
$rule = new \app\rbac\AuthorRule;
//添加一个规则
$auth->add($rule);
//添加"updateOwnPost"许可,并将此规则与它关联。
$updateOwnPost = $auth->createPermission('updateOwnArticle');
$updateOwnPost->description = '更新自己的文章';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
$updateOwnPost=$auth->getPermission('updateOwnPost');
//"updateOwnPost"将被"updatePost"使用
$updatePost = $auth->getPermission('updatePost');
$auth->addChild($updateOwnPost, $updatePost);
//允许作者更新自己的文章
$author= $auth->getRole('author');
$auth->addChild($author, $updateOwnPost);
现在我们得到了如下的层级关系:
许可->角色->用户(由低到高)
updatePost->admin->(Jane,ID=1)
createPost->author->(John,ID=2)
updatePost(AuthorRule)->author->(John,ID=2)
author->admin
//Access Check(存取检测) 有了前面的授权数据准备,存取检测只需调用yii\rbac\ManagerInterface::checkAccess()方法即可实现,因为绝大多数存取检测是针对于当前用户的,为了方便Yii提供了一个快捷方法yii\web\User::can(),使用方法如下:
if(\Yii::$app->user->can('createPost')){
//create post
}
如果当前用户是Jane(ID=1),我们从createPost开始,并尝试获取Jane的数据:
createPost->author->admin->(Jane,ID=1)
要检测一个用户可以更新一个文章,我们需要传一个额外的参数,此参数是前面编写的AuthorRule所需要的:
if(\Yii::$app->user->can('updatePost',['post'=>$post])){
//update post
}
下图是当前用户是John时发生的事情:
updatePost->updateOwnPost(AuthorRule)->author->John,ID=2
我们从updatePost开始,穿过updateOwnPost,为了通过存取检测,AuthorRule执行后返回了true,此方法从can()方法中接收了名为['post'=>$post]的参数,如果各项正常,我们将为John获得author的许可权限。
因为Jane是系统管理员(admin),对她的权限判断就简单一些:
updatePost->admin->Jane,ID=1
在控制器中有几个方法可以实现授权,如果你需要粒度许可以实现添加和删除权限的分离,你需要检查每个动作的权限,你可以在每个动作方法中使用上述条件判断,也可以使用
yii\filters\AccessControl:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => ['index'],
'roles' => ['managePost'],
],
[
'allow' => true,
'actions' => ['view'],
'roles' => ['viewPost'],
],
[
'allow' => true,
'actions' => ['create'],
'roles' => ['createPost'],
],
[
'allow' => true,
'actions' => ['update'],
'roles' => ['updatePost'],
],
[
'allow' => true,
'actions' => ['delete'],
'roles' => ['deletePost'],
],
],
],
];
}
如果所有的CRUD操作是在一起管理,那么使用单一的许可将是个好主意,如managePost,并在yii\web\Controller::beforeAction()中检查它。
Using Default Roles(使用默认角色)
默认角色是隐式分配给所有用户的角色,无需调用yii\rbac\ManagerInterface::assign(),授权数据也不用包含到它的分配信息中。 默认角色通常与一个规则相关联,此规则决定是否将角色应用于此被检查的用户上。 默认角色通常用于已经具有某种角色分配的应用程序。例如,一个应用可以在它的用户表中有一个group列,用于表示每个用户所属的权限组。如果每个权限组可以映射到一个RBAC角色,你可以使用默认角色来自动将用户分配到RBAC角色中,让我们使用一个例子来看一下这些是怎么实现的。
假设在user表中有一个group列,在此列中使用1代表管理组(admin),使用2代表写手组(author),你规划有两个RBAC角色admin和author来代表这两个组的许可,你可以使用如下代码构建RBAC数据:
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
/**
* Checks if user group matches
*/
class UserGroupRule extends Rule
{
public $name = 'userGroup';
public function execute($user, $item, $params)
{
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->group;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'author') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$author = $auth->createRole('author');
$author->ruleName = $rule->name;
$auth->add($author);
// ... add permissions as children of $author ...
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $author);
// ... add permissions as children of $admin ...
注意上面的代码,因为author被添加为admin的子项,当你实现规则类的execute()方法时,你需要特别留意这个层级关系,这也是为什么当角色名称是author时,如果用户组是1或2(意味着用户在admin组或author组中)execute()方法将返回true。 接下来配置authManger,将以下两个角色加入到yii\rbac\BaseManager::$defaultRoles 中:
return [
// ...
'components' => [
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'defaultRoles' => ['admin', 'author'],
],
// ...
],
];
现在如果你要执行一个权限检测,admin和author角色都将被检查,检查方法就是查询与他们相关的所有规则,也就是说此角色要应用到当前用户。基于以上的规则实现,如果一个用户的组的值是1,admin角色将被应用到用户,如果用户的组的值是2,author角色将被应用到用户。
(全文完)
共 2 条回复
阿江
最后登录:2024-03-03
在线时长:186小时21分
- 粉丝94
- 金钱16816
- 威望160
- 积分20276