tinywan

RBAC是什么,能解决什么难题?

RBAC是Role-Based Access Control的首字母,译成中文即基于角色的权限访问控制,说白了也就是用户通过角色与权限进行关联[其架构灵感来源于操作系统的GBAC(GROUP-Based Access Control)的权限管理控制]。简单的来说,一个用户可以拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。其对应关系如下:

image

在许多的实际应用中,系统不只是需要用户完成简单的注册,还需要对不同级别的用户对不同资源的访问具有不同的操作权限。且在企业开发中,权限管理系统也成了重复开发效率最高的一个模块之一。而在多套系统中,对应的权限管理只能满足自身系统的管理需要,无论是在数据库设计、权限访问和权限管理机制方式上都可能不同,这种不致性也就存在如下的憋端:

  • 维护多套系统,重复造轮子,时间没用在刀刃上
  • 用户管理、组织机制等数据重复维护,数据的完整性、一致性很难得到保障
  • 权限系统设计不同,概念理解不同,及相应技术差异,系统之间集成存在问题,单点登录难度大,也复杂的企业系统带来困难

RBAC是基于不断实践之后,提出的一个比较成熟的访问控制方案。实践表明,采用基于RBAC模型的权限管理系统具有以下优势:由于角色、权限之间的变化比角色、用户关系之间的变化相对要慢得多,减小了授权管理的复杂性,降低管理开销;而且能够灵活地支持应用系统的安全策略,并对应用系统的变化有很大的伸缩性;在操作上,权限分配直观、容易理解,便于使用;分级权限适合分层的用户级形式;重用性强。

ThinkPHP中RBAC实现体系

ThinkPHP中RBAC基于Java的Spring的Acegi安全系统作为参考原型,并做了相应的简化处理,以适应当前的ThinkPHP结构,提供一个多层、可定制的安全体系来为应用开发提供安全控制。安全体系中主要有以下几部分:

  • 安全拦截器
  • 认证管理器
  • 决策访问管理器
  • 运行身份管理器

安全拦截器

安全拦截器就好比一道道门,在系统的安全防护系统中可能存在很多不同的安全控制环节,一旦某个环节你未通过安全体系认证,那么安全拦截器就会实施拦截。

认证管理器

 

防护体系的第一道门就是认证管理器,认证管理器负责决定你是谁,一般它通过验证你的主体(通常是一个用户名)和你的凭证(通常是一个密码),或者更多的资料来做到。更简单的说,认证管理器验证你的身份是否在安全防护体系授权范围之内。

访问决策管理

 

虽然通过了认证管理器的身份验证,但是并不代表你可以在系统里面肆意妄为,因为你还需要通过访问决策管理这道门。访问决策管理器对用户进行授权,通过考虑你的身份认证信息和与受保护资源关联的安全属性决定是是否可以进入系统的某个模块,和进行某项操作。例如,安全规则规定只有主管才允许访问某个模块,而你并没有被授予主管权限,那么安全拦截器会拦截你的访问操作。 
决策访问管理器不能单独运行,必须首先依赖认证管理器进行身份确认,因此,在加载访问决策过滤器的时候已经包含了认证管理器和决策访问管理器。 
为了满足应用的不同需要,ThinkPHP 在进行访问决策管理的时候采用两种模式:登录模式和即时模式。登录模式,系统在用户登录的时候读取改用户所具备的授权信息到 Session,下次不再重新获取授权信息。也就是说即使管理员对该用户进行了权限修改,用户也必须在下次登录后才能生效。即时模式就是为了解决上面的问题,在每次访问系统的模块或者操作时候,进行即使验证该用户是否具有该模块和操作的授权,从更高程度上保障了系统的安全。

运行身份管理器

 

运行身份管理器的用处在大多数应用系统中是有限的,例如某个操作和模块需要多个身份的安全需求,运行身份管理器可以用另一个身份替换你目前的身份,从而允许你访问应用系统内部更深处的受保护对象。这一层安全体系目前的 RBAC 中尚未实现。

ThinkPHP中RBAC认证流程

对应上面的安全体系,ThinkPHP 的 RBAC 认证的过程大致如下:

  1. 判断当前模块的当前操作是否需要认证
  2. 如果需要认证并且尚未登录,跳到认证网关,如果已经登录 执行5
  3. 通过委托认证进行用户身份认证
  4. 获取用户的决策访问列表
  5. 判断当前用户是否具有访问权限

权限管理的具体实现过程

RBAC相关的数据库介绍

在ThinkPHP完整包,包含了RBAC处理类RBAC.class.php文件,位于Extend/Library/ORG/Util。打开该文件,其中就包含了使用RBAC必备的4张表,SQL语句如下(复制后请替换表前缀):

复制
CREATE TABLE IF NOT EXISTS `think_access` (
  `role_id` smallint(6) unsigned NOT NULL,
  `node_id` smallint(6) unsigned NOT NULL,
  `level` tinyint(1) 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 `think_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,
  PRIMARY KEY (`id`),
  KEY `level` (`level`),
  KEY `pid` (`pid`),
  KEY `status` (`status`),
  KEY `name` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `think_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,
  PRIMARY KEY (`id`),
  KEY `pid` (`pid`),
  KEY `status` (`status`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;

CREATE TABLE IF NOT EXISTS `think_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;

 

下面对RBAC相关的数据库表及字段作一下介绍:

表名 字段名 字段类型 作用
ly_user id INT 用户ID(唯一识别号)
username VARCHAR(16) 用户名
password VARCHAR(32) 密码
email VARCHAR(100) 用户邮箱
create_time TIMESTAMP 创建时间(时间戳)
logintime TIMESTAMP 最近一次登录时间(时间戳)
loginip VARCHAR(15) 最近登录的IP地址
status TINYINT(1) 启用状态:0:表示禁用;1:表示启用
remark VARCHAR(255) 备注信息
ly_role id INT 角色ID
name VARCHAR(20) 角色名称
pid SMALLINT(6) 父角色对应ID
status TINYINT(1) 启用状态(同上)
remark VARCHAR(255) 备注信息
ly_node id SMALLINT(6) 节点ID
name VARCHAR(20) 节点名称(英文名,对应应用控制器、应用、方法名)
title VARCHAR(50) 节点中文名(方便看懂)
status TINYINT(1) 启用状态(同上)
remark VARCHAR(255) 备注信息
sort SMALLINT(6) 排序值(默认值为50)
pid SMALLINT(6) 父节点ID(如:方法pid对应相应的控制器)
level TINYINT(1) 节点类型:1:表示应用(模块);2:表示控制器;3:表示方法
ly_role_user user_id INT 用户ID
role_id SMALLINT(6) 角色ID
ly_access role_id SMALLINT(6) 角色ID
node_id SMALLINT(6) 节点ID
level    
module    

以下是数据库表各字段的关联关系:

image

实现RBAC管理的前导性工作

基于ThinkPHP实现RBAC的权限管理系统中,首先要做一些前导性的工作(系统数据库设计TP已经为我们完成了),主要分以下几个方面:

  • 用户(增、删、改、查)
  • 角色(增、删、改、查)
  • 节点(增、删、改、查)
  • 配置权限(更新权限)

具体实现的代码如下(相关解释均在注释之中):

<?php
/**
 *
 */
namespace Home\Controller;

use Home\Controller\BaseController;
use Home\Model\AdminUserModel;
use Org\Util\Tree;
use Think\Page;

class RbacController extends BaseController
{
    //初始化操作
    public function _initialize()
    {
        if (!IS_AJAX) $this->error(\'你访问的页面不存在,请稍后再试\');
    }

    public function userIndex()
    {
        if (IS_POST) {
            $condition[\'username\'] = array(\'like\', "%" . trim(I(\'keybord\')) . "%");
            $model = D(\'AdminUser\');
            $count = $model->where($condition)->count();
            $Page = new Page($count, 3);
            $Page->setConfig(\'theme\', \'%FIRST% %UP_PAGE% %LINK_PAGE% %DOWN_PAGE% %END%\');
            $show = $Page->show();
            //select search
            $list = $model->where($condition)->field(\'password\', true)->relation(true)->limit($Page->firstRow . \',\' . $Page->listRows)->select();
            $this->show = $show;
            $this->list = $list;
            $this->display(\'AdminUser/index\');
        } else {
            $model = D(\'AdminUser\');
            $count = $model->count();
            $Page = new Page($count, 6);
            $Page->setConfig(\'header\', \'共%TOTAL_ROW%条\');
            $Page->setConfig(\'first\', \'首页\');
            $Page->setConfig(\'last\', \'共%TOTAL_PAGE%页\');
            $Page->setConfig(\'prev\', \'上一页\');
            $Page->setConfig(\'next\', \'下一页\');
            $Page->setConfig(\'link\', \'indexpagenumb\');
            $Page->setConfig(\'theme\', \'%FIRST% %UP_PAGE% %LINK_PAGE% %DOWN_PAGE% %END%\');
            $show = $Page->show();
            //select search
            $list = $model->field(\'password\', true)->relation(true)->limit($Page->firstRow . \',\' . $Page->listRows)->select();
            $this->show = $show;
            $this->list = $list;
            $this->display(\'Rbac/userIndex\');
        }

    }

    /*
    *  平台用户异步验证
    */
    public function checkUser()
    {
        $username = trim(I(\'username\'));
        $conditions = array(\'username\' => \':username\');
        $result = M(\'AdminUser\')->where($conditions)->bind(\':username\', $username)->find();
        //如果不存在,则可以创建,也就是返回的是true
        if (!$result) {
            echo \'true\';
        } else {
            echo \'false\';
        }
        exit();
    }

    /*
     *  创建平台用户,这里开启了服务器验证
     */
    public function createAdminUser()
    {
        if (IS_POST) {
            /**
             * [实例化User对象]
             * D方法实例化模型类的时候通常是实例化某个具体的模型类,如果你仅仅是对数据表进行基本的CURD操作的话,
             * 使用M方法实例化的话,由于不需要加载具体的模型类,所以性能会更高。
             */
            $model = D(\'AdminUser\');
            /**
             * 如果创建失败 表示验证没有通过 输出错误提示信息
             * $model->create() 会自动调用验证规则
             */
            if (!$model->create()) return $this->error($model->getError());
            //$username = $model->username;
            //$model->add() 插入数据后会自动的摧毁数据
            if (!$uid = $model->add()) return $this->success(\'注册失败\', U(\'Rbac/userIndex\'));//获取用户id
            // 如果是注册用户的话
//            session(\'uid\', $uid);
//            session(\'username\', $username);
            //用户添加成功后,给用户角色表添加数据
            $role[\'role_id\'] = I(\'role_id\');
            $role[\'user_id\'] = $uid;
            //添加该管理员操作到操作日志中
            $desc = \'给ID为:[\' . $_POST[\'role_id\'] . \']的角色,新增用户:[\' . $_POST[\'username\'] . \'],密码为:[\' . $_POST[\'password\'] . \']\' . \'其他参数\' . $_POST;
            addOperationLog($desc);
            if (D(\'AdminRoleUser\')->add($role)) {
                return $this->success(\'添加平台用户成功\', U(\'Rbac/userIndex\'));
            } else {
                return $this->error(\'添加平台用户失败\', U(\'Rbac/userIndex\'));
            }
            return $this->success(\'添加平台用户成功\', U(\'Rbac/userIndex\'));
        }
        $this->role_list = M(\'AdminRole\')->select();
        $this->display(\'Rbac/createAdminUser\');
    }

    /**
     * 改变用户角色
     * 可以支持不执行SQL而只是返回SQL语句:$User->fetchSql(true)->add($data);
     */
    public function updateUser()
    {
        $userId = I(\'get.id\');
        if (IS_POST) {
            $data[\'user_id\'] = I(\'post.user_id\');
            $data[\'role_id\'] = I(\'post.role_id\');
            $model = M(\'AdminRoleUser\');
            if ($model->where(array(\'user_id\' => $data[\'user_id\']))->delete() == false) {
                return $this->error(\'用户角色修改失败\', U(\'Rbac/updateUser\', array(\'id\' => $userId)));
            }
            if ($model->add($data) == false) {
                return $this->error(\'用户角色修改失败\', U(\'Rbac/updateUser\', array(\'id\' => $userId)));
            }
            return $this->success(\'用户角色修改成功\', U(\'Rbac/userIndex\'));
        }
        $this->role_list = M(\'AdminRole\')->select();
        $this->user = M(\'AdminUser\')->join(\'tour_admin_role_user ON tour_admin_role_user.user_id = tour_admin_user.id\')->where(array(\'id\' => $userId))->field(\'user_id,username,role_id\')->find();
        $this->display();
    }

    //删除用户
    public function delUser()
    {
        $user_id = I(\'post.id\', \'\', \'int\');
        $user = D(\'AdminUser\');
        $result = $user->relation(true)->where(array(\'id\' => $user_id))->delete();
        if ($result) {
            //添加该管理员操作到操作日志中
            $desc = \'删除用户ID:\' . $user_id . \'成功\';
            addOperationLog($desc);
            $response = [\'status\' => 200, \'errmsg\' => \'删除成功\', \'dataList\' => $result];
            return $this->ajaxReturn($response, \'JSON\');
        }
        //添加该管理员操作到操作日志中
        $desc = \'删除用户ID:\' . $user_id . \'失败\';
        addOperationLog($desc);
        $response = [\'status\' => 500, \'errmsg\' => \'删除失败\', \'dataList\' => $result];
        return $this->ajaxReturn($response, \'JSON\');

    }

    //设置用户状态
    public function userStatus()
    {
        $uid = I(\'post.id\');
        $db = M(\'AdminUser\');
        $status = $db->where(array(\'id\' => $uid))->getField(\'status\');
        $status = ($status == 1) ? 0 : 1;
        if ($db->where(array(\'id\' => $uid))->setField(\'status\', $status)) {
            $response = [\'status\' => 200, \'errmsg\' => \'改变成功\', \'dataList\' => $status];
            return $this->ajaxReturn($response, \'JSON\');
        }
        //添加该管理员操作到操作日志中
        $desc = \'设置用户状态:\' . $uid . \'失败\';
        addOperationLog($desc);
        $response = [\'status\' => 500, \'errmsg\' => \'改变失败\', \'dataList\' => $status];
        return $this->ajaxReturn($response, \'JSON\');
    }

    /***********************************节点开始****************************************************/
    public function nodeIndex()
    {
        $db = M(\'AdminNode\');
        $node = $db->order(\'id\')->select();
        $this->nodelist = Tree::create($node);
        $this->display(\'Rbac/nodeIndex\');
    }

    //创建权限表单处理
    public function createNode()
    {
        $db = M(\'AdminNode\');

        //创建权限表单处理
        if (IS_POST) {
            $db->create();
            if (!$db->add()) {
                return $this->error("权限添加失败", U(\'Rbac/nodeIndex\'));
            }
            return $this->success(\'权限添加成功\', U(\'Rbac/nodeIndex\'));
        }
        $node = $db->where(\'level !=3\')->order(\'sort\')->select();
        $this->nodelist = Tree::create($node);
        $this->display();
    }

    /*
    *   删除权限
    */
    public function delNode()
    {
        $result = M(\'AdminNode\')->where(array(\'id\' => I(\'post.id\', \'\', \'int\')))->delete();
        if ($result) {
            $response = [\'status\' => 200, \'errmsg\' => \'删除成功\', \'dataList\' => $result];
            return $this->ajaxReturn($response, \'JSON\');
        }
        $response = [\'status\' => 500, \'errmsg\' => \'删除失败\', \'dataList\' => $result];
        return $this->ajaxReturn($response, \'JSON\');
    }

    /*
    *   设置权限状态
    */
    public function NodeStatus()
    {
        $id = I(\'post.id\');
        $db = M(\'AdminNode\');
        $status = $db->where(array(\'id\' => $id))->getField(\'status\');

        $status = ($status == 1) ? 0 : 1;
        if ($db->where(array(\'id\' => $id))->setField(\'status\', $status)) {
            $response = [\'status\' => 200, \'errmsg\' => \'修改成功\', \'dataList\' => $status];
            return $this->ajaxReturn($response, \'JSON\');
        }
        $response = [\'status\' => 500, \'errmsg\' => \'修改失败\', \'dataList\' => $status];
        return $this->ajaxReturn($response, \'JSON\');
    }

    /*
   *   该节点是否在菜单栏显示
   */
    public function showMenus()
    {
        $id = I(\'post.id\');
        $db = M(\'AdminNode\');
        $show = $db->where(array(\'id\' => $id))->getField(\'menus\');
        $menus = ($show == 1) ? 0 : 1;
        $result = $db->where(array(\'id\' => $id))->setField(\'menus\', $menus);
        if ($result) {
            $response = [\'status\' => 200, \'errmsg\' => \'修改成功\', \'dataList\' => $result];
            return $this->ajaxReturn($response, \'JSON\');
        }
        $response = [\'status\' => 500, \'errmsg\' => \'修改失败\', \'dataList\' => $result];
        return $this->ajaxReturn($response, \'JSON\');
    }

    /***********************************角色开始****************************************************/
    public function roleIndex()
    {
        $db = M(\'AdminRole\');
        $this->rolelist = $db->select();
        $this->display();
    }

    /*
     *创建角色
     */
    public function createAdminRole()
    {
        if (IS_POST) {
            $name = I(\'post.name\', \'\', \'strip_tags\');
            $remark = I(\'post.remark\', \'\', \'strip_tags\');
            $pid = I(\'post.pid\', \'\', \'strip_tags\');  // 用strip_tags过滤$_GET[\'title\']
            if (empty($name)) return $this->error(\'角色名称不能为空\');
            $role = M(\'AdminRole\');
            $where[\'name\'] = \':name\';
            $roleName = $role->where($where)->bind(\':name\', $name, \PDO::PARAM_STR)->getField(\'name\');
            if ($roleName) return $this->error("角色名称:\'" . $name . "\'已经存在", U(\'Rbac/roleIndex\'));

            $role->name = $name;
            $role->remark = $remark;
            $role->pid = $pid;
            //create方法并不算是连贯操作,因为其返回值可能是布尔值,所以必须要进行严格判断。
            if ($role->create()) {
                // 如果主键是自动增长型 成功后返回值就是最新插入的值
                $result = $role->field(\'name,remark,pid\')->add();
                //如果在add方法之前调用field方法,则表示只允许写入指定的字段数据,其他非法字段将会被过滤
                if (!$result) return $this->error("角色添加失败", U(\'Rbac/createpartent\'));
                return $this->success(\'角色添加成功\', U(\'Rbac/roleIndex\'));
            }
            return $this->success(\'角色添加成功\', U(\'Rbac/roleIndex\'));
        }
        $this->display();
    }

    /*
     *添加权限Node位权限表,Access为权限-角色关联表
     */
    public function addNode()
    {
        $rid = I(\'rid\', \'\', \'int\');
        if (!is_numeric($rid)) return $this->success(\'参数类型错误,必须是数字\', U(\'Rbac/roleIndex\'));
        //getFieldById针对某个字段(ID)查询并返回某个字段(name)的值
        $roleModel = M(\'AdminRole\');
        $roleWhere[\'id\'] = \':id\';
        $role_name = $roleModel->where($roleWhere)->bind(\':id\', $rid, \PDO::PARAM_INT)->getField(\'name\');
        if ($role_name == false) return $this->success(\'没有找到该角色\', U(\'Rbac/roleIndex\'));

        //根据角色遍历所有权限
        $access = M(\'AdminAccess\');
        if (IS_POST) {
            $actions = I(\'post.actions\');
            try {
                $access->startTrans();
                $where[\'role_id\'] = \':role_id\';
                $mod1 = $access->where($where)->bind(\':role_id\', $rid)->delete();
                if (!$mod1) $access->rollback();
                $data = array();
                foreach ($actions as $value) {
                    $tmp = explode(\'_\', $value);
                    $data[] = array(
                        \'role_id\' => $rid,
                        \'node_id\' => $tmp[0],
                        \'level\' => $tmp[1]
                    );
                }
                if (!($access->addAll($data))) {
                    $access->rollback();
                } else {
                    $access->commit();
                }
                return $this->success(\'权限设置成功\', U(\'Rbac/addNode\', array(\'rid\' => $rid)));
            } catch (\Exception $e) {
                $access->rollback();
                return $this->success(\'权限设置异常\', U(\'Rbac/addNode\', array(\'rid\' => $rid)));
            }

        }
        $node = M(\'AdminNode\')->order(\'id\')->select();
        $node_list = Tree::create($node);
        $node_arr = array();
        foreach ($node_list as $value) {
            $conditions[\'node_id\'] = $value[\'id\'];
            $conditions[\'role_id\'] = $rid;
            $count = $access->where($conditions)->count();
            if ($count) {
                $value[\'access\'] = \'1\';
            } else {
                $value[\'access\'] = \'0\';
            }
            $node_arr[] = $value;
        }
        $this->role_name = $role_name;
        $this->node_list = $node_arr;
        $this->rid = $rid;
        $this->display();
    }

    /*
     *删除角色以及角色所拥有的权限
     */
    public function delRole()
    {
        $role_id = I(\'post.role_id\', \'\', \'int\');
        $user = D(\'AdminRole\');
        $result = $user->relation(true)->where(array(\'id\' => $role_id))->delete();
        if ($result) {
            $response = [\'status\' => 200, \'errmsg\' => \'修改成功\', \'dataList\' => $result];
            return $this->ajaxReturn($response, \'JSON\');
        }
        $response = [\'status\' => 500, \'errmsg\' => \'修改失败\', \'dataList\' => $result];
        return $this->ajaxReturn($response, \'JSON\');
    }

    /*
    *   设置角色状态
    */
    public function roleStatus()
    {
        $rid = I(\'post.rid\');
        $db = M(\'AdminRole\');
        $status = $db->where(array(\'id\' => $rid))->getField(\'status\');
        $status = ($status == 1) ? 0 : 1;
        if ($db->where(array(\'id\' => $rid))->setField(\'status\', $status)) {
            $response = [\'status\' => 200, \'errmsg\' => \'修改成功\', \'dataList\' => $status];
            return $this->ajaxReturn($response, \'JSON\');
        }
        $response = [\'status\' => 500, \'errmsg\' => \'修改失败\', \'dataList\' => $status];
        return $this->ajaxReturn($response, \'JSON\');
    }
}

 

ThinkPHP中RBAC类的详解

在ThinkPHP处理权限管理中,真正的难点并不是在RBAC类的使用上,上面相关的处理(权限配置,节点管理等);所以当完成以上工作,其实RBAC系统已经完成了90%。下面先从ThinkPHP中RBAC的配置说起(详细请参看对应的注释内容):

复制
<?php

 
return array(
 
"USER_AUTH_ON" => true, //是否开启权限验证(必配)
"USER_AUTH_TYPE" => 1, //验证方式(1、登录验证;2、实时验证)
 
"USER_AUTH_KEY" => \'uid\', //用户认证识别号(必配)
"ADMIN_AUTH_KEY" => \'superadmin\', //超级管理员识别号(必配)
"USER_AUTH_MODEL" => \'user\', //验证用户表模型 ly_user
\'USER_AUTH_GATEWAY\' => \'/Public/login\', //用户认证失败,跳转URL
 
\'AUTH_PWD_ENCODER\'=>\'md5\', //默认密码加密方式
 
"RBAC_SUPERADMIN" => \'admin\', //超级管理员名称
 
 
"NOT_AUTH_MODULE" => \'Index,Public\', //无需认证的控制器
"NOT_AUTH_ACTION" => \'index\', //无需认证的方法
 
\'REQUIRE_AUTH_MODULE\' => \'\', //默认需要认证的模块
\'REQUIRE_AUTH_ACTION\' => \'\', //默认需要认证的动作
 
\'GUEST_AUTH_ON\' => false, //是否开启游客授权访问
\'GUEST_AUTH_ID\' => 0, //游客标记
 
"RBAC_ROLE_TABLE" => \'ly_role\', //角色表名称(必配)
"RBAC_USER_TABLE" => \'ly_role_user\', //用户角色中间表名称(必配)
"RBAC_ACCESS_TABLE" => \'ly_access\', //权限表名称(必配)
"RBAC_NODE_TABLE" => \'ly_node\', //节点表名称(必配)
);

注意:

  • 以上有的配置项并非必须的,但标有“必配”,则必须配置
  • 无需认证的权限和方法与需要认证的模块和动作可以根据需要选择性配置,还有部分权限相关配置并未列表出

RBAC处理类提供静态的方法

ThinkPHP的RBAC处理类提供给我们很多相关的静态方法如下:

方法名 接收参数说明 返回值 说明
RBAC::authenticate($map,$model=\'\');
  • $map:ThinkPHP数据库处理类的where条件参数
  • $model:用户表名,默认取配置文件C(\'USER_AUTH_MODEL\')
array

RBAC::authenticate(array("username"=>"admin","userpwd" => I(\'POST.pwd\',\'\', \'md5\')));

返回值是在用户表中,以$map为条件where的查阅结果集

0RBAC::saveAccessList($authId=null);
  • $authId:用户识别号,默认取C(\'USER_AUTH_KEY\');
返回一个空值 如果验证方式为登录验证,则将权限写入session中,否则不作任何处理
RBAC::getRecordAccessList($authId=null,$module=\'\');
  • $authId:用户识别号(可不传)
  • $module:当前操作的模块名称
Array 返回一个包含权限的ID的数组
RBAC::checkAccess() 返回true或false 检查当前操作是否需要认证(根据配置中需要认证和不需要评论的模块或方法得出)
RBAC::checkLogin() true 如果当前操作需要认证且用户没有登录,继续检测是否开启游客授权。如果开启游客授权,则写入游客权限;否则跳到登录页
RBAC::AccessDecision($appName=APP_NAME)
  • $appName:选传,有默认值
  • true:表示有操作权限
  • false:无操作权限
AccessDecision($appName=APP_NAME)方法,检测当前项目模块操作,是否存在于$_SESSION[\'_ACCESS_LIST\']数组中$_SESSION[\'_ACCESS_LIST\'][\'当前操作\'][\'当前模块\'][\'当前操作\']是否存在。如果存在表示有权限,返回true;否则返回flase。
RBAC::getAccessList($authId)
  • $authId:用户识别号(选传,程序自动获取)
Array 通过数据库查询取得当前认证号的所有权限列表
RBAC::getModuleAccessList($authId,$module)
  • $authId:用户识别号(必)
  • $module:对应的模块(必)
Array 返回指定用户可访问的节点权限数组

注意:在使用RBAC::AccessDecision()方法时,如果你开启了项目分组,则必须传入当前分组,代码如下:

复制
  1. //权限验证
  2. if(C(\'USER_AUTH_ON\') && !$notAuth) {
  3. import(\'ORG.Util.RBAC\');
  4. //使用了项目分组,则必须引入GROUP_NAME
  5. RBAC::AccessDecision(GROUP_NAME) || $this->error("你没有对应的权限");
  6. }

RBAC处理类的实际应用

在完成用户登录,角色创建,节点增删改查的工作后,就只剩下了RBAC如何在对应程序代码中应用了。挻简单的,只用在原来的代码其他上改动几个地方即可。

  • 用户登录时,写入用户权限
  • 用户操作时,进行权限验证

下面是用户登录时的实现代码:

复制
<?php
namespace Home\Controller;

use Think\Controller;
use Org\Util\Rbac;

class LoginController extends Controller
{
    public function index()
    {
        $this->display();
    }

    /*
     * 异步验证账号
    */
    public function checkUser()
    {
        $username = I(\'username\');
        $conditions = array(\'username\' => \':username\');
        $result = M(\'User\')->where($conditions)->bind(\':username\', $username)->find();
        //如果不存在,则可以创建,也就是返回的是true
        if (!$result) {
            echo \'false\';
        } else {
            echo \'true\';
        }
        exit();
    }

    /*
     * 异步验证密码
    */
    public function checkPwd()
    {
        $username = I(\'post.username\');
        $password = I(\'post.password\');
        $conditions = array(\'username\' => \':username\');
        $result = M(\'User\')->where($conditions)->bind(\':username\', $username)->find();
        if (md5($password) != $result[\'password\']) {
            echo \'false\';
        } else {
            echo \'true\';
        }
        exit();
    }

    /*
     * 检查登录
    */
    public function checkLogin()
    {
        if (!IS_POST) $this->error(\'非法访问\');

        // 采用htmlspecialchars方法对$_GET[\'name\'] 进行过滤,如果不存在则返回空字符串
        $username = I(\'post.username\', \'\', \'htmlspecialchars\');
        // 采用正则表达式进行变量过滤,如果正则匹配不通过的话,则返回默认值。
        //I(\'get.name\',\'\',\'/^[A-Za-z]+$/\');
        $password = md5(I(\'post.password\'));

        $user = D(\'AdminUser\');
        $where = array(\'username\' => $username);
        $fields = array(\'id\', \'password\', \'username\', \'status\', \'expire\', \'logintime\'); // 之查找需要的字段
        $result = $user->where($where)->field($fields)->find();

        if (!$result || $password != $result[\'password\']) return $this->error(\'账号或密码错误\',U(\'Home/Login/index\'));

        if ($result[\'status\'] == 0) return $this->error(\'该用户被锁定,暂时不可登录\',U(\'Home/Login/index\'));

        // 是否记住我的登录,设置一个Cookie,写在客户端
        if (isset($_POST[\'remember\'])) {
            $value = $result[\'id\'] . \'|\' . get_client_ip() . \'|\' . $result[\'username\'];
            $value = encrytion($value, 1);
            @setcookie(\'remember\', $value, C(\'AUTO_LOGIN_LIFETIME\'), \'/\');
        }

        // 每天登录增加经验值
        $today = strtotime(date(\'Y-m-d\')); // 获取今天0时0分0秒的时间
        // 如果上次的登录时间小于今天的时间,则增加经验值
        $where2 = array(\'id\' => $result[\'id\']);
        if ($result[\'logintime\'] < $today) {
            $user->where($where2)->setInc(\'expire\', 10);
        }

        //更新登录户登录信息
        $data_arr = array(
            \'id\' => $result[\'id\'],
            \'logintime\' => time(),
            \'loginip\' => get_client_ip(),
        );
        if ($user->save($data_arr)) {
            // 获取$_SESSION[\'user_id\'] 如果不存在则默认为0
            session(\'uid\', 0);
            session(\'username\', $result[\'username\']);
            session(\'loginAccount\', $result[\'username\']);
            session(\'loginUserName\', $result[\'username\']);
            session(\'uid\', $result[\'id\']);
            //RBAC 开始,用户认证SESSION标记 ,默认为"authId"
            session(C(\'USER_AUTH_KEY\'), $result[\'id\']);

            //如果为超级管理员,则无需验证
            if ($_SESSION[\'username\'] == C(\'RBAC_SUPERADMIN\')) session(C(\'ADMIN_AUTH_KEY\'), true);

            //用于检测用户权限的方法,并保存到Session中,读取用户权限
            Rbac::saveAccessList($result[\'id\']);
            //添加操作日志中
            $desc = \'登陆成功\';
            addOperationLog($desc);
            return $this->redirect(\'Index/index\');
        } else {
            return $this->error(\'2222222222222\');
        }

    }

    public function memberInfo()
    {
        $user_id = session(\'user_id\');
        $user = D(\'User\');
        $where = array(\'user_id\' => $user_id);
        $result = $user->where($where)->select();
        $this->result = $result;
        $this->display();
    }

    /**
     * 获取apikey_values
     */

    public function apikey()
    {
        $secret = "6JNVkTk4jHsgF0e1oOVLwOZDeq83pDXa";
        $user_id = I(\'user_id\', \'\', int);
        // $where = array(\'user_id = %d \',array($user_id));
        $user = M(\'User\');
        // $result = $user->where($where)->find();
        $result = $user->where("user_id = %d", array($user_id))->find();

        $hash = sha1($result[\'user_id\'] . $result[\'password\'] . $secret);
        $data = array(
            \'apikey_value\' => $hash,
            \'apikey_time\' => time()
        );
        $where = array(\'user_id\' => $user_id);
        $user->where($where)->save($data);
        $this->ajaxReturn($hash);
    }

    public function hash()
    {
        if (!IS_POST) {
            $out_data = array(
                \'err_msg\' => \'request method is error.\',
                \'is_success\' => \'Fail\'
            );
            exit(json_encode($out_data));
        };

        $username = I(\'username\');
        $password = I(\'password\');
        $where = array(\'username\' => $username);

        $user = M(\'User\');
        $result = $user->where($where)->find();
        if (!$result || $password != $result[\'password\']) {
            $out_data = array(
                \'err_msg\' => \'Username or password is incorrect.\',
                \'is_success\' => \'Fail\'
            );
            exit(json_encode($out_data));
        }

        /**
         * HASH生成规则
         */
        $secret = "6JNVkTk4jHsgF0e1oOVLwOZDeq83pDXa";
        $hash = sha1($result[\'user_id\'] . $result[\'password\'] . $secret);

        $where = array(\'user_id\' => $result[\'user_id\']);
        $data["apikey_value"] = $hash;
        $data["apikey_time"] = time();
        $user->where($where)->save($data);

        $out_data = array(
            \'is_success\' => \'Success\',
            \'hash\' => $hash
        );
        exit(json_encode($out_data));
    }

    public function relationTest()
    {
        echo get_client_ip();
    }

    public function error1(){
        $this->display();
    }

    public function logout()
    {
        session(\'username\', NULL);
        session_unset();
        session_destroy();
        return $this->redirect(\'Login/index\');
    }
}

 

接着在控制器目录创建一个CommonAction.class.php文件,然后改写所有要权限验证的类,让其继承自CommonAction。代码如下:

复制
<?php
namespace Home\Controller;

use Think\Controller;
use Org\Util\Rbac;
class BaseController extends Controller
{
//    /**
//     * ThikPHP自动运行方法,每一次,都会自动运行这个方法
//     * 要判断一个session值是否已经设置,可以使用
//       session(\'?name\'); 相当于:isset($_SESSION[\'name\']);
//     */
    public function _initialize(){
        /***************************************网站开关****************************************************/
        if(!C(\'WEB_STATE\')) exit(\'网站维护中\');

        /***********************************没有登录时候时需要执行的代码**************************************/
        if(!isset($_SESSION[C(\'USER_AUTH_KEY\')])) return $this->redirect(\'Login/index\');

        /***************************************自动登录配置************************************************/
        if(isset($_COOKIE[\'remember\']) && !isset($_SESSION[\'uid\'])){
            // Cookie 解密
            $value = encrytion($_COOKIE[\'remember\']);
            $result = explode(\'|\',$value);
            // 判断IP地址是否一样
            if($result[1] == get_client_ip()){
                session(\'uid\',$result[0]);
                session(\'uid\',$result[2]);
            }
        }

        /***************************************权限认证****************************************************/
        $Public = in_array(MODULE_NAME,explode(\',\',C(\'NOT_AUTH_MODULE\'))) || in_array(ACTION_NAME,explode(\',\',C(\'NOT_AUTH_ACTION\')));
        // 如果不在公共模块之中,同时开启权限验证的话,则开始认证过程
        if(C(\'USER_AUTH_ON\') && !$Public)
        {
            if(!Rbac::AccessDecision()) //通过accessDecision获取权限信息,true:表示有操作权限,false:无操作权限
            {
                return $this->error("你没有对应的权限");  //没有获取到权限信息时需要执行的代码
            }
        }
        /***************************************导航栏菜单显示****************************************************/
        /*
        *   思路:
        *   1.取出所有权限节点。
        *   2.取出当前登录用户拥有的模块权限(取英文名称)和操作权限(取ID)
        *   3.对所有权限进行遍历,先匹配模块权限,不存在删除,存在则匹配操作权限
        */
        // 超级管理员登录
        if(session(C(\'ADMIN_AUTH_KEY\')))
        {
            $menus = D(\'AdminNode\')->where(\'level = 2\')->relation(true)->order(\'sort desc\')->select();//取出所有节点
        }else{
            /**
             * [1]取出所有的权限,是通过关联模型从数据库中获取的
             */
            $menus = D(\'AdminNode\')->where(\'level = 2\')->relation(true)->order(\'sort desc\')->select();
            $module = \'\';    //存放拥有的模块
            $node_id = \'\';   //存放拥有的模块
            /**
             * [2]获取当前用户的所有权限,这个是从RBAC中获取的
             */
            $access_list = $_SESSION[\'_ACCESS_LIST\'];   //当前用户所拥有的权限
            foreach ($access_list as $key => $value) {
                foreach ($value as $key1 => $value1) {
                    $module = $module.\',\'.$key1;    //字符串拼接模块名称
                    foreach ($value1 as $key2 => $value2) {
                        $node_id = $node_id.\',\'.$value2;    //字符串拼操作id
                    }
                }
            }
            /**
             * [3]去除没有权限的节点,通过所有权限和用户已经拥有的权限比较
             */
            foreach ($menus as $key => $value) {
                $all_node[] = $value[\'name\'];
                if(!in_array(strtoupper($value[\'name\']), explode(\',\', $module))){
                    unset($menus[$key]);    //删除模块
                }else{
                    //模块存在,比较里面的操作
                    foreach ($value[\'node\'] as $key1 => $value1) {
                        if(!in_array($value1[\'id\'], explode(\',\', $node_id))){
                            unset($menus[$key][\'node\'][$key1]);  // 删除操作
                        }
                    }
                }
            }
        }
        $this->menus = $menus;
    }

}

ThinkPHP中提供了一个_initialize()方法,是在类初始化就会执行的,也就是只要后面控制器继承自CommonAction类,就会在作对应操作时,执行_initialize()方法。

分类:

技术点:

相关文章: