说到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' => 'mdmadminModule', 'layout' => '@app/views/layouts/main_nifty',//yii2-admin的导航菜单 ],
添加添加authManager配置项:
需要强调的是,yii中的authManager组件有PhpManager和DbManager两种方式,这两种方式是由区别的,PhpManager将权限关系保存在文件里,DbManager方式,将权限关系保存在数据库。我们采用保存在数据库中的方式。
'authManager' => [ 'class' => 'yiirbacDbManager', // or use 'yiirbacDbManager' ],
添加as access:
'as access' => [
'class' => 'mdmadmincomponentsAccessControl',
'allowActions' => [
// add or remove allowed actions to this list
// 'admin
public static function getMenuKeyByUserId($user_id)
{
if (empty($user_id)) {
return false;
}
$list = (new yiidbQuery())->select('**')
->from('**')
->where(['user_id' => $user_id])
->all();
if (empty($list)) {
return false;
}
$role_str = '';
foreach ($list as $key => $value) {
$role_str .= $value['item_name'];
}
$redis_key = Yii::$app->params['key'] . md5($role_str . Yii::$app->db->dsn);
return $redis_key;
}
public static function UpdateMenuVersion()
{
$version_key = Yii::$app->params['key'] . md5(Yii::$app->params['key'] . Yii::$app->db->dsn);
$version_val = Yii::$app->redis->get($version_key);
if (empty($version_val)) {
$version_val = '1';
} else {
$version_val++;
}
$log = json_encode([
'user_id' => Yii::$app->user->id,
'version_key' => $version_key,
'version_val' => $version_val
]);
writeLog($log, 'update_menu_version');
Yii::$app->redis->set($version_key, $version_val);
}
2、导航的高亮,图标,是否显示
默认的导航高亮是按照模块,控制器,方法来进行直接匹配的,这样一来有一种需求无法满足,比如:A控制器下得页面下载B控制器下面高亮,这种事无法实现的,所以要修改他们高亮机制。我们没有再采用他的高亮逻辑,而是自己实现了一个新的逻辑。我首先把要高亮的页面url加入到菜单的data里面,data是一个json数据,如下所示:
{"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']) ? $data['visible'] : '';
//菜单隐藏的逻辑
if (empty($return['visible'])) {
return false;
}
$return['icon'] = isset($data['icon']) ? $data['icon'] : '';
//控制菜单打开的逻辑
$return['openurl'] = isset($data['openurl']) ? $data['openurl'] : '';
$items && $return['items'] = $items;
return $return;
};
3、重写权限检测
刚才已经说了,yii-admin 的权限检测执行太费时间,执行SQL太多,所以我打算重写他的权限检查的方法,通过读源码可以看到,他们检查是通过user中的can方法调用的,然后通过mdmadmincomponentsAccessControl中的beforeAction实现的,我们可以看一下:
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() . '
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分到非管理员权限的人员(有坑)
//匿名方法,处理管理员返回值的情况
//检查是否是管理员, 管理员都有权限
//根据用户去取权限
$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执行太慢可以添加缓存,缓存过期的时间和菜单过期类似,当用户的权限有变动的时候和菜单修改的时候跟新缓存。两一种解决办法是把这个方法协程单利,利用单利只是执行一次权限的查询,检查的阶段可以单独写成方法提供。



