ThinkPHP3.2的权限控制RBAC

Published on 2016 - 12 - 05

概念

基于角色的访问控制(Role-Based Access Control)。它作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。

在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。 在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。

作用

区分不同角色的可见资源、可操作资源。如老板可以控制最大、财务可以看见并处理财务相关的报表。研发人员可以看见整个系统的组成模块、导航和配置之类的。哪些操作谁该直接处理,这些操作的权限就应该给哪个角色。

组成

安全拦截器

RBAC::checkAccess 方法返回当前请求的操纵是否需要认证。

认证管理器

RBAC::authenticate 识别不同的身份,你的用户名、密码、权限是否在授权范围内。

决策访问管理器

RBAC::AccessDecision, 必须同认证管理器一同使用

分为即时模式和登录模式。即时模式指修改了权限后,下次操作访问时权限检测立即生效,登录模式则必须退出后再登录权限列表才会是新的。其实就是一个是每次读表查询出数据,一个是读取之前登录时获取的权限列表的session缓存。

运行身份管理器

单身份、多身份管理B/S。单身份指一个用户只有一个角色,多身份指一个用户具有多个身份,并且某个操作权限要求极高必须多个身份的权限才能使用。所以多身份管理一般不存在。

原理

  1. 判断当前的操作(项目【应用】,模块、动作(操作)是否需要认证)对应节点(Node表里和配置里和AUTH相关的配置如NOT_AUTH_MODULE、REQUIRE_AUTH_MODULE、NOT_AUTH_ACTION、REQUIRE_AUTH_ACTION也就是需要认证的模块、不需要认证的模块、需要认证的操作和不需要认证的操作啦)。
  2. 如果需要认证(判断用户是否登录,如果没登录-->跳至委托认证管理器验证身份,判断用户是否有权限访问如果没权限--直接跳至无权访问页面)
  3. 委托认证来验证用户身份
  4. 获取该用户的权限列表
  5. 判断用户是否有权限访问

如何使用

为了方便大家更好的理解,我在参考了http://www.thinkphp.cn/code/714.html源码后,将年久失修的ThinkPHP RBAC示列美化增强了一下。我会结合示列给大家讲解如何正确使用ThinkPHP的RBAC。

确认类库文件存在

ThinkPHP中使用RBAC挺简单,只要你的第三方类库里有RBAC类文件,。那么在你要使用RBAC的类的控制器里写上命名空间

use Org\Util\Rbac;

就可以使用RBAC::checkAccess 的这些方法了。
当然这只是类文件的准备,要使用RBAC我们得准备5张表。

建表

user(用户)、role(角色)、node(节点)、role_user(角色用户关联表)、access(角色权限关联表)

user用户表

除了必备的 id、account、password 之外,其他字段和权限无关。

role角色表

id、name、status、remark备注 是必要的, 其他的也可以不要。有可能角色存在上下级关系,我觉得很少会用到。

role_user 角色关联表

role_id、user_id 只用来关联的

node 节点

id、name、title、status、remark 这个是rbac 最终认证的最小单位, 其实最小单位是 APP_NAME.Controller_NAME.ACTION_NAME。

access 权限关联表

role_id、node_id、level、pid用于存放每个节点可以操作的节点id

以上表的建表sql RBAC类里注释已经帮我们准备好了,注意原文件access表的少了一个pid字段。

CREATE TABLE IF NOT EXISTS `rbac_user` (
  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `account` varchar(64) NOT NULL,
  `account` varchar(64) NOT NULL,
  `nickname` varchar(50) NOT NULL,
  `password` char(32) NOT NULL,
  `bind_account` varchar(50) NOT NULL,
  `last_login_time` int(11) unsigned DEFAULT '0',
  `last_login_ip` varchar(40) DEFAULT NULL,
  `login_count` mediumint(8) unsigned DEFAULT '0',
  `verify` varchar(32) DEFAULT NULL,
  `email` varchar(50) NOT NULL,
  `remark` varchar(255) NOT NULL,
  `create_time` int(11) unsigned NOT NULL,
  `update_time` int(11) unsigned NOT NULL,
  `status` tinyint(1) DEFAULT '0',
  `type_id` tinyint(2) unsigned DEFAULT '0',
  `info` text NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `account` (`account`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=35 ;

CREATE TABLE IF NOT EXISTS `rbac_role` (
  `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `pid` smallint(6) DEFAULT NULL,
  `status` tinyint(1) unsigned DEFAULT NULL,
  `remark` varchar(255) DEFAULT NULL,
  `ename` varchar(5) DEFAULT NULL,
  `create_time` int(11) unsigned NOT NULL,
  `update_time` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `parentId` (`pid`),
  KEY `ename` (`ename`),
  KEY `status` (`status`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=8 ;

CREATE TABLE IF NOT EXISTS `rbac_role_user` (
  `role_id` mediumint(9) unsigned DEFAULT NULL,
  `user_id` char(32) DEFAULT NULL,
  KEY `group_id` (`role_id`),
  KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

CREATE TABLE IF NOT EXISTS `rbac_access` (
  `role_id` smallint(6) unsigned NOT NULL,
  `node_id` smallint(6) unsigned NOT NULL,
  `level` tinyint(1) NOT NULL,
  `pid` smallint(6) NOT NULL,
  `module` varchar(50) DEFAULT NULL,
  KEY `groupId` (`role_id`),
  KEY `nodeId` (`node_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `rbac_node` (
  `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `title` varchar(50) DEFAULT NULL,
  `status` tinyint(1) DEFAULT '0',
  `remark` varchar(255) DEFAULT NULL,
  `sort` smallint(6) unsigned DEFAULT NULL,
  `pid` smallint(6) unsigned NOT NULL,
  `level` tinyint(1) unsigned NOT NULL,
  `type` tinyint(1) NOT NULL DEFAULT '0',
  `group_id` tinyint(3) unsigned DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `level` (`level`),
  KEY `pid` (`pid`),
  KEY `status` (`status`),
  KEY `name` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=83 ;

各个表的关系图:

level共分成3个等级,level = 1表示项目,如Home,Admin.
level = 2 表示对应的Controller,level = 3表示Controller中的Action.

更改配置

将RBAC类注释里的配置拿到Home的conf.php中,

'USER_AUTH_ON'        => true,
'USER_AUTH_TYPE'      => 2,     // 默认认证类型 1 登录认证 2 实时认证
'USER_AUTH_KEY'       => 'authId',  // 用户认证SESSION标记
'ADMIN_AUTH_KEY'      => 'administrator',
'USER_AUTH_MODEL'     => 'User',    // 默认验证数据表模型
'AUTH_PWD_ENCODER'    => 'md5', // 用户认证密码加密方式
'USER_AUTH_GATEWAY'   => '/Public/login',// 默认认证网关
'NOT_AUTH_MODULE'     => 'Public',  // 默认无需认证模块
'REQUIRE_AUTH_MODULE' => '',        // 默认需要认证模块
'NOT_AUTH_ACTION'     => '',        // 默认无需认证操作
'REQUIRE_AUTH_ACTION' => '',        // 默认需要认证操作
'GUEST_AUTH_ON'       => false,    // 是否开启游客授权访问
'GUEST_AUTH_ID'       => 0,        // 游客的用户ID
'DB_LIKE_FIELDS'      => 'title|remark',
'RBAC_ROLE_TABLE'     => 'rbac_role',
'RBAC_USER_TABLE'     => 'rbac_role_user',
'RBAC_ACCESS_TABLE'   => 'rbac_access',
'RBAC_NODE_TABLE'     => 'rbac_node',

添加登录后权限获取缓存

ThinkPHP的RBAC 权限列表依赖于SESSION。并且权限是在登录后获取访问权限列表的。

我们看下登录方法,到底做了什么。

    //登录页面
    public function login(){
        if(IS_POST){
            if(empty($_POST['username'])) {
                $this->error('帐号错误!');
            }elseif (empty($_POST['password'])){
                $this->error('密码必须!');
            }
            //生成认证条件
            $map = array();
            // 支持使用绑定帐号登录
            $map['account'] = $_POST['username'];
            $map["status"] = array('gt',0);
            //使用用户名、密码和状态的方式进行认证
            $authInfo = RBAC::authenticate($map);
            if(false === $authInfo) {
                $this->error('帐号不存在或已禁用!');
            }else {
                if($authInfo['password'] != md5($_POST['password'])) {
                    $this->error('密码错误!');
                }
                $_SESSION[C('USER_AUTH_KEY')] = $authInfo['id'];
                if($authInfo['username'] == C('ADMIN_AUTH_KEY')) {
                    $_SESSION['administrator'] = true;
                }
                //保存登录信息
                $User = M('User');
                $ip = get_client_ip();
                $time = time();
                $data = array();
                $data['id'] = $authInfo['id'];
                $data['last_login_time'] = $time;
                $data['login_count'] = array('exp','login_count+1');
                $data['last_login_ip'] = $ip;
                $User->save($data);
                // 缓存访问权限
                RBAC::saveAccessList();
                $this->success('登录成功!', U('/'));
            }
        }else{
            $this->display();
        }
    }
  • 显示用认证管理器获取user表里存在的对应用户信息。
//生成认证条件
$map = array();
// 支持使用绑定帐号登录
$map['account'] = $_POST['username'];
$map["status"] = array('gt',0);
//使用用户名、密码和状态的方式进行认证
$authInfo = RBAC::authenticate($map);
  • 然后判断账号是否存在、密码是否正确。身份存在的话将用户表id赋值给$_SESSION[C('USER_AUTH_KEY')],并判断是否是超级管理员 。
if($authInfo['username'] == C('ADMIN_AUTH_KEY')) {
    $_SESSION['administrator'] = true;
}
  • 最关键的一步,缓存权限,如果是即时模式,缓存无效,最好写上,方便认证模式切换。
// 缓存访问权限
RBAC::saveAccessList();

看下saveAccessList方法:

//用于检测用户权限的方法,并保存到Session中
static function saveAccessList($authId=null) {
    if(null===$authId)   $authId = $_SESSION[C('USER_AUTH_KEY')];
    // 如果使用普通权限模式,保存当前用户的访问权限列表
    // 对管理员开放所有权限
    if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] )
        $_SESSION['_ACCESS_LIST']   =   self::getAccessList($authId);
    return ;
}

非即时模式、并且不是超级管理员时才更新session。

我们再看下getAccessList方法。

static public function getAccessList($authId) {
    // Db方式权限数据
    $db     =   Db::getInstance(C('RBAC_DB_DSN'));
    $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE'));
    $sql    =   "select node.id,node.name from ".
                $table['role']." as role,".
                $table['user']." as user,".
                $table['access']." as access ,".
                $table['node']." as node ".
                "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=1 and node.status=1";
    $apps =   $db->query($sql);
    $access =  array();
    foreach($apps as $key=>$app) {
        $appId  =   $app['id'];
        $appName     =   $app['name'];
        // 读取项目的模块权限
        $access[strtoupper($appName)]   =  array();
        $sql    =   "select node.id,node.name from ".
                $table['role']." as role,".
                $table['user']." as user,".
                $table['access']." as access ,".
                $table['node']." as node ".
                "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=2 and node.pid={$appId} and node.status=1";
        $modules =   $db->query($sql);
        // 判断是否存在公共模块的权限
        $publicAction  = array();
        foreach($modules as $key=>$module) {
            $moduleId    =   $module['id'];
            $moduleName = $module['name'];
            if('PUBLIC'== strtoupper($moduleName)) {
                $sql    =   "select node.id,node.name from ".
                $table['role']." as role,".
                $table['user']." as user,".
                $table['access']." as access ,".
                $table['node']." as node ".
                "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1";
                $rs =   $db->query($sql);
                foreach ($rs as $a){
                    $publicAction[$a['name']]    =   $a['id'];
                }
                unset($modules[$key]);
                break;
            }
        }
        // 依次读取模块的操作权限
        foreach($modules as $key=>$module) {
            $moduleId    =   $module['id'];
            $moduleName = $module['name'];
            $sql    =   "select node.id,node.name from ".
                $table['role']." as role,".
                $table['user']." as user,".
                $table['access']." as access ,".
                $table['node']." as node ".
                "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1";
            $rs =   $db->query($sql);
            $action = array();
            foreach ($rs as $a){
                $action[$a['name']]  =   $a['id'];
            }
            // 和公共模块的操作权限合并
            $action += $publicAction;
            $access[strtoupper($appName)][strtoupper($moduleName)]   =  array_change_key_case($action,CASE_UPPER);
        }
    }
    return $access;
}

其实过程就是先查出为当前用户授权过的应用列表(或叫项目),然后再遍历项目获取授权过的模块,最后遍历模块获取授权过的操作。
最后得到的数组类似:

最里层的都是节点id。这样一个多维数组生成后,缓存到session里。

将要进行权限判断的控制器继承Common控制器,自动绑上初始化进行模块/控制器/操作对应的权限判断

<?php
namespace Home\Controller;
use Think\Controller;
use Think\Page;
use Org\Util\Rbac;
//如果不需要权限验证的控制器不要继承它
class CommonController extends Controller{

    //对权限的验证和判断
    public function _initialize() {
        define('__URL__', __CONTROLLER__);
        // 用户权限检查
        if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) {
            if (!RBAC::AccessDecision()) {
                //检查认证识别后
                if (!$_SESSION [C('USER_AUTH_KEY')]) {
                    //跳转到认证网关
                    redirect(PHP_FILE . C('USER_AUTH_GATEWAY'));
                }
                // 没有权限 抛出错误
                if (C('RBAC_ERROR_PAGE')) {
                    // 定义权限错误页面
                    redirect(C('RBAC_ERROR_PAGE'));
                } else {
                    if (C('GUEST_AUTH_ON')) {
                        $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY'));
                    }
                    // 提示错误信息
                    $this->error(L('_VALID_ACCESS_'));
                }
            }
        }
    }

用RBAC类进行权限判断:

if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) {

当开启RBAC判断并且模块不在无需认证模块中时执行权限判断。
用RBAC::AccessDecision() 决策访问管理器进行判断。
没通过判断,进行错误引导。

if (!$_SESSION [C('USER_AUTH_KEY')]) {
    //跳转到认证网关
    redirect(PHP_FILE . C('USER_AUTH_GATEWAY'));
}

如果没有认证的session表示没登录,跳到登录页面进行授权。

// 没有权限 抛出错误
if (C('RBAC_ERROR_PAGE')) {
    // 定义权限错误页面
    redirect(C('RBAC_ERROR_PAGE'));
} else {
    if (C('GUEST_AUTH_ON')) {
       $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY'));
    }
    // 提示错误信息
    $this->error(L('_VALID_ACCESS_'));
}

登录了没权限,如果定义了权限报错页面,去那。没定义的话如果开启游客模式,跳到登录网关。没有开游客访问模式直接提示无权限。

所以这段代码关键处在于判断是否有权限,我们看AccessDescision方法。

//权限认证的过滤器方法
static public function AccessDecision($appName=MODULE_NAME) {
    //检查是否需要认证
    if(self::checkAccess()) {
        //存在认证识别号,则进行进一步的访问决策
        $accessGuid   =   md5($appName.CONTROLLER_NAME.ACTION_NAME);
        if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) {
            if(C('USER_AUTH_TYPE')==2) {
                //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效
                //通过数据库进行访问检查
                $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]);
            }else {
                // 如果是管理员或者当前操作已经认证过,无需再次认证
                if( $_SESSION[$accessGuid]) {
                    return true;
                }
                //登录验证模式,比较登录后保存的权限访问列表
                $accessList = $_SESSION['_ACCESS_LIST'];
            }
            //判断是否为组件化模式,如果是,验证其全模块名
            if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) {
                $_SESSION[$accessGuid]  =   false;
                return false;
            }
            else {
                $_SESSION[$accessGuid]  =   true;
            }
        }else{
            //管理员无需认证
            return true;
        }
    }
    return true;
}

self::checkAccess()先判断当前模块下控制器下操作是否需要判断。如果不需要咱直接通过返回true。

需要的话,先通过md5 将 $appName.CONTROLLER_NAME.ACTION_NAME 加密生成一个guid。

然后先通过session里ADMIN_AUTH_KEY 判断是不是超级管理员,超级管理员直接返回true,具有所有权限。

不是超级管理员。

if(C('USER_AUTH_TYPE')==2) {
    //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效
    //通过数据库进行访问检查
    $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]);
}else {
    // 如果是管理员或者当前操作已经认证过,无需再次认证
    if( $_SESSION[$accessGuid]) {
        return true;
    }
    //登录验证模式,比较登录后保存的权限访问列表
    $accessList = $_SESSION['_ACCESS_LIST'];
    //判断是否为组件化模式,如果是,验证其全模块名
}
 if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) {
        $_SESSION[$accessGuid]  =   false;
        return false;
    }
    else {
        $_SESSION[$accessGuid]  =   true;
    }

看当前认证模式是否是即时,即时就重新获取,非即时的如果当前guid在session存在说明认证过了,不存在读取访问认证权限缓存列表。然后就是简单的数组判断看授权列表里有无当前[模块][控制器][操作]的节点。 有就将session里guid 设为true,没有就设为false。

退出时清除整个session。
整个RBAC 认证流程就结束了。

RBAC附加功能的实现

其实RBAC的难点在于数据库的理解和节点添加、角色授权。

从数据的先后顺序上来说先是添加节点,然后新增用户,新增角色,最后给角色授权。

添加节点

这里注意的是节点是权限判断的基础数据,由于程序上的设计,必须保证上下级顺序才能对应查询时生成对应的多为数组供以判断。所以其实大家只要记住先添加模块再添加控制器最后添加操作即可。

这里我只特别说下这个下拉树的实现。

在示列的NodeController 中, 写了一个 _before_add 和_before_edit 用于编辑和显示页面获取上级列表树:

// 获取配置类型
public function _before_add() {
    $model = M("Node");
    $list = $model->where('status=1')->select();
    $node_tree = D('Tree')->toFormatTree($list);
    $this->assign('node_tree', $node_tree);
    $this->assign('pid', I('pid', 1));
}

public function _before_edit() {
    $this->_before_add();
}

主要诀窍就在这个TreeModel里。

我们看把所有开启状态的节点查出来的列表怎么通过toFormatTree变成了有层级先后顺序显示的下拉。

public function toFormatTree($list, $title = 'title', $pk = 'id', $pid = 'pid', $root = 0) {
    $list = list_to_tree($list, $pk, $pid, '_child', $root);
    $this->formatTree = array();
    $this->_toFormatTree($list, 0, $title);
    return $this->formatTree;
}

首先将列表转换为无限级tree。这个需求很常见。用了以前老板tp了扩展函数里的list_to_tree:

/**
 * 把返回的数据集转换成Tree
 * @access public
 * @param array $list 要转换的数据集
 * @param string $pid parent标记字段
 * @param string $level level标记字段
 * @return array
 */
function list_to_tree($list, $pk = 'id', $pid = 'pid', $child = '_child', $root = 0) {
    // 创建Tree
    $tree = array();
    if (is_array($list)) {
        // 创建基于主键的数组引用
        $refer = array();
        foreach ($list as $key => $data) {
            $refer[$data[$pk]] = & $list[$key];
        }
        foreach ($list as $key => $data) {
            // 判断是否存在parent
            $parentId = $data[$pid];
            if ($root == $parentId) {
                $tree[] = & $list[$key];
            } else {
                if (isset($refer[$parentId])) {
                    $parent = & $refer[$parentId];
                    $parent[$child][] = & $list[$key];
                }
            }
        }
    }
    return $tree;
}

转换成树以后,调用_toFormatTree进行格式转换。

/**
 * 将格式数组转换为树
 *
 * @param array $list
 * @param integer $level 进行递归时传递用的参数
 */
private $formatTree; //用于树型数组完成递归格式的全局变量

private function _toFormatTree($list, $level = 0, $title = 'title') {
    foreach ($list as $key => $val) {
        $tmp_str = str_repeat(" ", $level * 1);
        $tmp_str.="└";
        $val['level'] = $level;
        $val['title_show'] = $level == 0 ? $val[$title] : $tmp_str . $val[$title];
        if (!array_key_exists('_child', $val)) {
            array_push($this->formatTree, $val);
        } else {
            $tmp_ary = $val['_child'];
            unset($val['_child']);
            array_push($this->formatTree, $val);
            $this->_toFormatTree($tmp_ary, $level + 1, $title); //进行下一层递归
        }
    }
    return;
}

遍历每条数据,根据level算出前面要补多少空格。如果没有_child子树就添加到formatTree属性里。有的话递归调用_toFormatTree,传入时讲level+1了。这样子级的前导空格始终比父级多一个。

添加角色

添加用户

角色授权

实现方法就是,显示把模块和对应操作遍历出来,并且获取对应角色的权限列表后,输出json,给前端进行匹配勾选。将勾选的节点id保存在rule数组里传递。

保存时:

public function saveAccessList(){
    $groupId = I('groupID');
    $model = D('Role');
    $apps = $model->getGroupAppList($groupId);
    $moduleList = $model->getGroupModuleList($groupId, $apps[0]['id']);
    foreach ($moduleList as $key => $module) {
        $model->delGroupAction($groupId, $module['id']);
    }
    $res = $model->setGroupActions($groupId, I('rule'));
    if($res)
        $this->success('更新成功');
    else
        $this->error('更新失败');
}

先按模块删除权限列表。然后再保存该角色的可用操作节点列表。

getGroupModuleList和delGroupAction及setGroupModules这些都是官网老rbac示列里的方法。

function getGroupModuleList($groupId,$appId) {
    $table = $this->tablePrefix.'access';
    $rs = $this->db->query('select b.id,b.title,b.name from '.$table.' as a ,'.$this->tablePrefix.'node as b where a.node_id=b.id and  b.pid='.$appId.' and a.role_id='.$groupId.' ');
    return $rs;
}

function setGroupModules($groupId,$moduleIdList) {
    if(empty($moduleIdList)) {
        return true;
    }
    if(is_array($moduleIdList)) {
        $moduleIdList = implode(',',$moduleIdList);
    }
    $where = 'a.id ='.$groupId.' AND b.id in('.$moduleIdList.')';
    $rs = $this->db->execute('INSERT INTO '.$this->tablePrefix.'access (role_id,node_id,pid,level) SELECT a.id, b.id,b.pid,b.level FROM '.$this->tablePrefix.'role a, '.$this->tablePrefix.'node b WHERE '.$where);
    if($result===false) {
        return false;
    }else {
        return true;
    }
}

function delGroupAction($groupId,$moduleId) {
    $table = $this->tablePrefix.'access';

    $result = $this->db->execute('delete from '.$table.' where level=3 and pid='.$moduleId.' and role_id='.$groupId);
    if($result===false) {
        return false;
    }else {
        return true;
    }
}

这样一个角色授权在一个页面内就可选择并保存完毕了。不用来回切模块、操作。

真正授权的苦力活就是添加节点了。其实那里可以优化用前端树组件做到一个页面ajax管理树节点。大家自己深入吧。

参考文档