说到CMS,最需要有的东西就是权限控制,特别是一些复杂的场景,多用户,多角色,多部门,子父级查看等等。最近在开发一个线下销售的东东,这个系统分为管理员端,省代端,客户端,门店端,销售端, 部门端,部门老大下面分子部门等等,恶心的需求。我们这个项目使用yii框架开发,yii在php届还是比较流行的,虽然说laravel现在横行,但是一些部门一些团队还是采用了yii框架,比如我们。
我是刚接触yii这个框架,开始的时候对这种面向组件的框架甚是别扭。当时打算自己写权限的,自己创建权限表,关联表等,但是学习使用yii开发文档后,发现有个权限控制RBAC,借助于yii-admin可以实现完美的权限,菜单的控制。这篇博客分两部门,第一部分我会讲述怎么搭建权限管理包括:安装yii-admin,创建权限表,使用权限控制菜单和访问权限等基本的操作,这部分大致说一下,想要看更详细的步骤可以参考这个比较详细的讲解:http://www.manks.top/tag/rbac.html,毕竟搭建和使用都不是难事,只要按照步骤来。第二部分我会讲解我自己的理解,包括:菜单的优化,子页面导航的选择性高亮,分角色显示菜单,权限检测的改进等。
一、yii-admin的搭建相关
1、搭建yii-admin
首先你应该安装一个yii矿建,因为yii-admin是基于yii框架的,没有框架玩毛啊!你可以在github上直接下载源码
yii2:https://github.com/yiisoft/yii2
yii2-admin:https://github.com/mdmsoft/yii2-admin
当然你可以使用composer来安装,这样最好不过,如果你安装好了yii,你就可以切换到项目目录下,直接执行下面的命令:
php composer.phar require mdmsoft/yii2-admin "~2.0" php composer.phar update
然后配置中加入yii-admin的配置项,值的注意的是如果yii2-admin配置在common目录下是全局生效,那么你在执行命令控制台的时候就会报错,所以应将权限控制作用于web模块,我们这个项目没有使用高级模板,所以你可以直接把配置写在config下面的web.php中,配置如下:
先定义别名:
'aliases' => [ '@mdm/admin' => '@vendor/mdmsoft/yii2-admin', ],
在modules中添加admin组件:
'admin' => [ 'class' => 'mdm\admin\Module', 'layout' => '@app/views/layouts/main_nifty',//yii2-admin的导航菜单 ],
添加添加authManager配置项:
需要强调的是,yii中的authManager组件有PhpManager和DbManager两种方式,这两种方式是由区别的,PhpManager将权限关系保存在文件里,DbManager方式,将权限关系保存在数据库。我们采用保存在数据库中的方式。
'authManager' => [ 'class' => 'yii\rbac\DbManager', // or use 'yii\rbac\DbManager' ],
添加as access:
'as access' => [ 'class' => 'mdm\admin\components\AccessControl', 'allowActions' => [ // add or remove allowed actions to this list // 'admin/*', //'*', 'site/*', 'api/*', ] ],
需要说的是未知不要放错了,如下图所示:
2、配置数据库权限表
这一步不用自己去写,命令行切换到yii2目录,执行下面命令,创建rbac需要的表,但是数据库需要自己创建名字是:yii2basic,如果要执行命令,就需要把你刚下配置好的配置文件在在console.php中也写一份,如果执行不成功,可以吧生成数据表的脚本拿出来自己执行。
yii migrate --migrationPath=@yii/rbac/migrations yii migrate --migrationPath=@mdm/admin/migrations
如果执行成功会生成5张表,还需要一张user表,你可以自己添加
menu //菜单表
auth_rule //规则表
auth_item_child //角色对应的权限,parent角色,child权限名
auth_item //角色、权限表,type=1表示角色,type=2表示权限
auth_assignment //角色与用户对应关系表
如果全部成功的话,再访问index.php"text-align: center">
3、进行菜单控制
要进行菜单控制,就需要用到刚才创建的那几个表中的menu表,左侧的导航按照我们的设计应该可以通过权限进行控制,写死的导航不能达到目的,可扩展行还不强,所以菜单控制必须要支持。
需要注意的是,如果你的后台框架中用到了自己的layout,你需要自己去指定,我们这个项目就是,有我们自己的layout,上面再添加admin组件的时候已经添加了:
'layout' => '@app/views/layouts/main_nifty',
然后我们操作菜单列表。添加菜单项,然后在打开layout文件,其实获取菜单的逻辑已经写好了,在MenuHelper中,添加命名空间mdm\admin\components\MenuHelper; 然后注销原来的导航,添加下面的代码,基本上就可以实现权限-用户-导航的控制了。
echo Nav::widget( [ "encodeLabels" => false, "options" => ["class" => "sidebar-menu"], "items" => MenuHelper::getAssignedMenu(Yii::$app->user->id), ] );
好了说完了,最后看一下这个页面:
二、yii-admin优化和重写
在使用的过程中,yii-admin实现的导航权限控制远不能满足我们的需求,并且,这种组件试的开发,每个操作是完全独立的,比如,检查权限,取菜单,取用户信息,每个操作都需要执行SQL来进行下面是正常的检查权限和得到菜单的sql执行过程。其实这个过程是极其费时的,当用户量比较多,菜单比较大,权限表中的数据非常多的时候是不能这样干的,使用我们自己的sql检测工具可以看到,这个过程执行了20条之多的sql语句:
在图中可以看出,权限检查涉及了14次的sql查询,菜单涉及了5次sql查询,如此多的sql 执行一旦上线事没有什么并发可言的。yii-admin这个组件提供了方便的权限控制,菜单控制,但是性能上面我们不敢苟同。查看源码你就知道,这个组件在我看来是一个解耦比较高的组件,每个成分之间可以单独的使用,这就需要每个操作必须要有自己独立的数据库来源,说白了就需要每次都执行sql去取到想要的值,中间很少使用连表查询这样的sql,其实10条sql做的功能,在耦合上网情况下,一条sql就搞定了。
像我这种人是不能忍受这么多不相关的sql执行的,所以我就在根源上面修改了yii-admin的权限检查部分,修改的方法是我自己想的,不一定对,也不一定适合所有的场景,下面就写出来与大家分享。
1、菜单的优化
我们通过查看菜单的生成过程大致会执行了5条以上的sql,这个还算可以,我没有做sql上的优化,原因是我们的菜单是要对应不同的角色和子父级关系,在原来的基础上我添加了一个type来区分是那种角色能看到这种菜单,一级哪种角色对应某一个菜单显示的层级关系。这样管理员,省代用户,客户都会呈现不同的菜单。即使配置相同的权限,不同层级的用户也会看到不同的菜单。
我们的优化是缓存菜单的生成数据,我们这个菜单是定制的,没有采用一开始配置的Nav::widget来呈现,而是我们自己循环层级关系,这样虽然麻烦,但是能很好的提取菜单中我们需要的没一个逻辑,比如:面包屑的自动生成,就可以每次提取菜单的label,再比如子页面,不同控制器下得左导航的高亮,下面是代码,php和html混写了,以后会慢慢的提取。
<ul class="nav nav-list"> <"<"> <a href="<"> <i class="menu-icon fa fa-<"></i> <span class="menu-text"> <"arrow"></b> </li> <"<"> <a href="index.html"data-target="#multi-cols-<"class="dropdown-toggle"> <i class="menu-icon fa fa-<"></i> <span class="menu-text"> <"arrow fa fa-angle-down"></b> </a> <b class="arrow"></b> <ul id="multi-cols-<" class="submenu"> <"<"> <a href="<"> <i class="menu-icon fa fa-caret-right"></i> <"arrow"></b> </li> <"<"> <a href="#" class="dropdown-toggle"> <i class="menu-icon fa fa-caret-right"></i> <"arrow fa fa-angle-down"></b> </a> <b class="arrow"></b> <ul class="submenu"> <"<"> <a href="<"> <i class="menu-icon fa fa-caret-right"></i> <"arrow"></b> </li> <"htmlcode">$user_id = Yii::$app->user->id; $breadcrumb = []; $menus_new['list'] = MenuHelper::getAssignedMenu($user_id); $redis_key = MenuHelper::getMenuKeyByUserId($user_id); $redis_menu = Yii::$app->redis->get($redis_key); $redis_varsion = getVersion(); if (!empty($redis_menu)) { $menus_new = json_decode($redis_menu, true); $old_version = isset($menus_new['version']) "htmlcode">{"icon": "fa fa-home", "visible": true, "openurl":"/web/site/index/"}这样我们通过openurl就能知道哪个导航高亮,在页面中直接判断当前请求的url在不在这个openurl里面就可以,但是这样做有缺点,必须要有把高亮的页面加入到要高亮的导航里面,如果页面太多这种方式不怎么好,但是我没有想到更好的方法去解决,如果哪位大神有好的方法可以在评论中写出,非常感谢。
图标和可见性的控制可以借助于MenuHelper中getAssignedMenu的回调方法实现,你可以在调用该方法的时候传入回调方法,我直接写的匿名方法,添加在了该方法里面,如下所示:
$user_type = Yii::$app->user->identity->type; $customer_id = Yii::$app->user->identity->customer_id; $callback_func = function($menu) use ($user_type, $customer_id) { $data = json_decode($menu['data'], true); $items = $menu['children']; $return = [ 'label' => $menu['name'], 'url' => [$menu['route']], ]; $return['visible'] = isset($data['visible']) "htmlcode">/** * @inheritdoc */ public function beforeAction($action) { $actionId = $action->getUniqueId(); $user = $this->getUser(); //预留系统检查权限的逻辑,一旦重写检查权限失败,调用系统检查权限的方法 if ($user->can('/' . $actionId)) { return true; } $obj = $action->controller; do { if ($user->can('/' . ltrim($obj->getUniqueId() . '/*', '/'))) { return true; } $obj = $obj->module; } while ($obj !== null); $this->denyAccess($user); }因为全权限的检查包含了子父级检查,也就是说 /admin/menu/update的权限是对/admin/menu/* 和/admin/* 和 /*都可见的,所以我们会看到$user->can的调用会使用do -while来进行,这样就增加的检查的复杂度,执行的sql就会批量的增加,你想啊,没一个父级的检查都是一次全新的函数调用,所以最恶心的也莫过于此了,感兴趣的同学可以去看看他的这个过程,当你自己调用这个函数检测的时候就会发现,执行的sql不是一般的多。
下面是我的重写方法,一条SQL,兼容了权限,角色,批量检查和未登录用户的权限检查,具体实现如下:
/** * 权限判断方法 (先不要使用该方法,用的系统方法,效率极低,等有时间重写之后再用) * @param string/array $permission_name 权限值(URL 或者 权限名)/批量检测可以传入数组 * @param int $user 用户id,不传值会取当前的登陆用户 * @return boolen * @author zhaoyafei */ public static function permissionCheck($permission_name, $user = 0) { //检查是否登陆过 if (Yii::$app->user->isGuest) { Yii::$app->response->redirect('/site/login'); } if (empty($permission_name)) { return false; } if (empty($user)) { $user = Yii::$app->user->id; } //管理员权限不能直接返回true,会存在管理员type = 1分到非管理员权限的人员(有坑) //匿名方法,处理管理员返回值的情况 /*$setAdminSet = function($param) use ($permission_name) { $paramtmp = $permission_name; if (is_array($paramtmp)) { if (count($paramtmp) == 1) { return true; } $paramtmp = array_flip($paramtmp); foreach ($paramtmp as $key => &$value) { $value = true; } } else { $paramtmp = true; } return $paramtmp; };*/ //检查是否是管理员, 管理员都有权限 /*if (empty($user)) { $user = Yii::$app->user->id; $user_type = Yii::$app->user->identity->type; if ($user_type == TYPE_ADMIN) { return $setAdminSet($permission_name); } } else { $user_sql = "SELECT type FROM xm_user WHERE id = :id"; $user_info = Yii::$app->db->createCommand($user_sql)->bindValue(":id", $user)->queryOne(); if (empty($user_info)) { return false; } if ($user_info['type'] == TYPE_ADMIN) { return $setAdminSet($permission_name); } }*/ //根据用户去取权限 $permission_list = []; $sql = "SELECT xc.child, xc1.child as role_name FROM xm_auth_assignment xa INNER JOIN xm_auth_item_child xc ON xa.item_name = xc.parent LEFT JOIN xm_auth_item_child xc1 ON xc.child = xc1.parent WHERE xa.user_id = :user_id"; $permission = Yii::$app->db->createCommand($sql) ->bindValue(":user_id", $user) ->queryAll(); if (empty($permission)) { return false; } //组合权限列表 foreach ($permission as $key => $value) { if (!empty($value['child']) && !in_array($value['child'], $permission_list)) { $permission_list[] = $value['child']; } if (!empty($value['role_name']) && !in_array($value['role_name'], $permission_list)) { $permission_list[] = $value['role_name']; } } //匿名方法,处理子url生成 $getUrlList = function($url) { if (!strstr($url, '/')) { return [$url]; } $url = '/' . trim($url, '/'); $params = explode('/', $url); $param_arr = []; $param_str = []; if (!empty($params) && is_array($params)) { foreach ($params as $key => $value) { if (!empty($value)) { $param_arr[] = $value; } } } if (!empty($param_arr)) { $tmp_str = ''; $param_str[] = $url; $count = count($param_arr); //生成子父级关系 for ($i = $count -1; $i >= 0; $i--) { $tmp_str = '/' . $param_arr[$i] . $tmp_str; $chold_url = str_replace($tmp_str, '/*', $url); if (!in_array($chold_url, $param_str)) { $param_str[] = $chold_url; } } } return $param_str; }; //拼接检查数据,兼容单传和传输组的情况 $check_list = []; if (is_array($permission_name)) { foreach ($permission_name as $key => $value) { $check_list[$value] = $getUrlList($value); } } else { $check_list[$permission_name] = $getUrlList($permission_name); } if (empty($check_list)) { return false; } //批量检查是否有权限 $ret = []; foreach ($check_list as $key => $value) { $ret[$key] = false; foreach ($value as $k => $v) { if (in_array($v, $permission_list)) { $ret[$key] = true; break; } } } //兼容一维数组 if (count($ret) == 1) { $ret = array_values($ret); return $ret[0]; } return $ret; }需要说明的是,注释掉的部分是管理员的权限检查,如果是管理员会自动返回所有的权限,但是这种不太好,因为实际情况中会分多种管理员,这样管理员不一定拥有所有的权限,如果这样不是超级管理员就不能使用,所以用的时候还是要慎重,最好统一使用权限检查。如果感觉那个SQL执行太慢可以添加缓存,缓存过期的时间和菜单过期类似,当用户的权限有变动的时候和菜单修改的时候跟新缓存。两一种解决办法是把这个方法协程单利,利用单利只是执行一次权限的查询,检查的阶段可以单独写成方法提供。
华山资源网 Design By www.eoogi.com
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
更新日志
- 柏菲·珞叔作品集《金色大厅2》限量开盘母带ORMCD[低速原抓WAV+CUE]
- Gareth.T《sad songs(Explicit)》[320K/MP3][29.03MB]
- Gareth.T《sad songs(Explicit)》[FLAC/分轨][152.85MB]
- 证声音乐图书馆《海风摇曳·盛夏爵士曲》[320K/MP3][63.06MB]
- 龚玥《金装龚玥HQCD》头版限量[WAV分轨]
- 李小春《吻别》萨克斯演奏经典[原抓WAV+CUE]
- 齐秦《辉煌30年24K珍藏版》2CD[WAV+CUE]
- 证声音乐图书馆《海风摇曳·盛夏爵士曲》[FLAC/分轨][321.47MB]
- 群星 《世界经典汽车音乐》 [WAV分轨][1G]
- 冷漠.2011 《冷漠的爱DSD》[WAV+CUE][1.2G]
- 陈明《流金岁月精逊【中唱】【WAV+CUE】
- 群星《Jazz-Ladies1-2爵士女伶1-2》HQCD/2CD[原抓WAV+CUE]
- 群星《美女私房歌》(黑胶)[WAV分轨]
- 郑源.2009《试音天碟》24BIT-96KHZ[WAV+CUE][1.2G]
- 飞利浦试音碟 《环球群星监听录》SACD香港版[WAV+CUE][1.1G]