1. 程式人生 > >Thinkphp中的RBAC許可權驗證

Thinkphp中的RBAC許可權驗證

一、相關概念

訪問控制與RBAC模型
1、訪問控制:
通常的多使用者系統都會涉及到訪問控制,所謂訪問控制,是指通過某種方式允許活限制使用者訪問能力及範圍的一種方法。這主要是由於系統需要對關鍵資源進行保護,防止由於非法入侵或者誤操作對業務系統造成破壞。簡而言之,訪問控制即哪些使用者可以訪問哪些資源。
一般而言,訪問控制系統包括三個組成部分:
主體:發出訪問請求的實體,通常指使用者或者使用者程序。
客體:被保護的資源,通常是程式,資料,檔案或者裝置。
訪問策略:主體和客體的對映規則,確定一個主體是否對客體具有訪問能力。

2RBAC (Role-Base-Access-Controll)
基於角色的訪問控制(

RBAC)的概念在七十年代就已經提出,但是直到九十年代由於安全需求的發展才又引起了廣泛關注。RBAC 的核心思想是將系統資源的訪問許可權進行分類或者建立層次關係,抽象為角色的概念,然後根據安全策略將使用者和角色關聯,從而實現了使用者和許可權之間的對照。 RBAC 通過引入角色並將其作為許可權管理的中介,將訪問控制系統分為兩個部分,即許可權與角色的關聯和角色與使用者的關聯,具有靈活易控制的優點。
資料來源

我的理解就是給使用者賦予不同的角色,給角色服務不同的許可權,許可權可以看成要訪問資源的集合,角色只是使用者和許可權的過渡層,可能會問為什要有一個角色 過渡層,可以試想一下沒有role過渡層,有多個使用者有著相同的數個許可權,分配管理許可權的時候,要重複分配許可權,工作量很大,更何況修改許可權的時候,更麻 煩,可以說不利於程式碼的維護,所以要有一個

role的過渡層。下面來看看TPthinkphp)中的RBAC的實現及用法。

二、TP中的RBAC類及使用RBAC
1RBAC許可權驗證的大致流程:
、驗證當前操作是否需要驗證
、驗證是否登入
、檢視當前使用者的身份
、獲取當前使用者的許可權列表
、進行許可權驗證

2RBAC所依賴的5張資料表

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;

CREATE TABLE `think_user` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `name` varchar(20) DEFAULT '',

  `password` varchar(25) NOT NULL,

  `phone_num` varchar(20) DEFAULT NULL,

  `register_time` int(11) DEFAULT NULL,

  `login_time` int(11) DEFAULT NULL,

  `login_ip` varchar(20) DEFAULT NULL,

  `status` tinyint(1) DEFAULT NULL,

  PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;

資料表模型如下:

這五張表的關係如下:
一個使用者對應著多個角色;一個角色可以屬於對個使用者;是多對多的關係,需要用箇中間表即role_user表;
一個角色可以有多個許可權;一個許可權可以屬於多個使用者;是多對多的關係,需要有個中間表即access表;

node表示什麼?
node表是記錄許可權的表,說白了就是記錄應用,控制器,及方法的表,即要訪問資源的集合;比如要訪問的Admin應用下的Index控制器下的index方法;從這裡可以明確了TPRBAC是控制使用者對控制器及方法的訪問許可權進行許可權的管理。下面來看個圖,加深理解:

這張圖裡面的使用者,角色,結點,許可權及使用者與角色中間表一一對應即可(這圖是從網上盜的)。其實還可將使用者進行抽象,對其進行分組,不同組別裡面的角色不同,這可一根據不同的業務進行嘗試。可以參考這篇部落格
知道了RBAC需要的5張資料表,下面來看看RBAC類是如何實現許可權驗證的。

3RBAC
RBAC類中三個核心的方法:
①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;

    }

可以看出方法是依次獲取專案模組,控制器,動作方法的許可權的。

②saveAccessList()檢測使用者的許可權列表,並將許可權儲存到SESSION

//用於檢測使用者許可權的方法,並儲存到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 ;

    }

    ③AccessDecision()許可權決策,就是判斷使用者擁有哪些許可權//許可權認證的過濾器方法

    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;

    }

從程式碼中可以看出,許可權判斷是有兩種方式一種是根據session中的許可權列表進行校驗,一種是每次都查詢資料庫進行校驗(下面配置引數說明的時候會說明)

array(3) {

  ["phone"] => string(11) "18792455613"

  ["uid"] => string(2) "13"

  ["_ACCESS_LIST"] => array(1) {

    ["ADMIN"] => array(5) {

      ["ARTICLE"] => array(0) {

      }

      ["CATEGORY"] => array(3) {

        ["CATEGORYLIST"] => string(2) "15"

        ["ADDCATEGORY"] => string(2) "16"

        ["ALTERCATEGORY"] => string(2) "17"

      }

      ["MEDIA"] => array(2) {

        ["IMAGE"] => string(2) "12"

        ["VIDEO"] => string(2) "13"

      }

      ["INDEX"] => array(5) {

        ["USERLIST"] => string(1) "7"

        ["ADDUSER"] => string(1) "8"

        ["ADDROLE"] => string(1) "9"

        ["ADDNODE"] => string(2) "10"

        ["INDEX"] => string(2) "11"

      }

      ["DATA"] => array(0) {

      }

    }

  }}

AccessDecision()方法中呼叫checkAccess()方法進行驗證模組的過濾即去除不需要驗證的模組,控制器和方法。知道了RBAC的實現思路,下面來使用RBAC類進行許可權驗證。

4、使用RBAC類進行許可權驗證

首先處理5張表,就是對5張表的增刪該查

實現邏輯程式碼如下:建立一個RbacController.class.php的控制器

 /**

     * 角色列表檢視

     */

    public function role(){

        $roleList = M("role")->select();

        $this->assign("roleList",$roleList);

        layout("base");

        $this->display();

    }

    /**

     * 角色表單處理

     */

    public function roleHandle(){

        $id = I("get.id");

        if(M("role")->where(array("id"=>$id))->delete()){

            $this->success("刪除成功",U("Admin/Index/role"));

        }else{

            $this->error("刪除失敗");

        }

    }

    /**

     * 結點列表檢視

     */

    public function node(){

        $field = array('id','name','title','pid');

        $node = M('node')->field($field)->order('sort')->select();

        $this->node = node_merge($node);

        layout("base");

        $this->display();

    }

    /**

     * 許可權列表檢視

     */

    public function access(){

        $rid = I('rid',0,"intval");

        $field = array('id','name','title','pid');

        $node = M('node')->order('sort')->field($field)->select();

        //原有的許可權

        $access = M('access')->where(array('role_id'=>$rid))->getField('node_id',true);

        $this->node = node_merge($node,$access);

        // dump($this->node);

        $this->rid = $rid;

        layout("base");

        $this->display();

    }

    /**

     * 設定許可權表單處理

     */

    public function setAccess(){

        $rid = I('rid',0,"intval");

        $db = M('access');

        $db->where(array('role_id'=>$rid))->delete();

        $data = array();

        foreach ($_POST['access'] as $v ) {

            $tmp = explode('_', $v);

            $data[] = array(

                'role_id' => $rid,

                'node_id' => $tmp[0],

                'level' => $tmp[1]

            );

        }

        if ($db->addAll($data)) {

            $this->success('修改成功',U('Admin/Index/role'));

        }else{

            $this->error('修改失敗');

        }

    }

    /**

     * 新增角色檢視

     */

    public function addRole(){

        layout("base");

        $this->display();

    }

    /**

     * 新增角色表單處理

     */

    public function addRoleHandle(){

        $data = I("post.");

        if(M("role")->data($data)->add()){

            $this->success("新增成功",U("Admin/Index/role"));

        }else{

            $this->error("新增角色失敗,請稍後再試");

        }

    }

    /**

     * 新增結點檢視

     */

    public function addNode(){

        $this->pid = I("pid",0,"intval");

        $this->level = I("level",1,"intval");

        switch ($this->level){

            case 1:

                $this->type = "應用";

                break;

            case 2:

                $this->type = "控制器";

                break;

            case 3:

                $this->type = "方法";

                break;

        }

        layout("base");

        $this->display();

    }

    /**

     * 新增結點表單處理

     */

    public function addNodeHandle(){

        $data = I("post.");

        if(M("node")->data($data)->add()){

            $this->success("新增成功",U("Admin/Index/node"));

        }else{

            $this->error("新增失敗,請稍後再試");

        }

    }

其中有一個結點和許可權整合的函式node_merge(),實現的思路是遞迴進行結點新增,將統一控制器中的不同方法整合在一起,程式碼如下:這段程式碼放在Common資料夾下的function.php

/**

 * 許可權結點整合

 * @param  array   $node   結點陣列

 * @param  array   $access 許可權陣列

 * @param  integer $pid    父級ID

 * @return array           組合後的陣列

 */function node_merge($node,$access=null,$pid=0){

    $arr = array();

    foreach($node as $v){

        if(is_array($access)){

            $v['access'] = in_array($v['id'],$access) ? 1 : 0;

        }

        if($pid==$v['pid']){

            $v['child'] = node_merge($node,$access,$v['id']);

            $arr[] = $v;

        }

    }

    return $arr;}

完成了準備工作可以使用RBAC類了,首先先對RABC進行引數配置,下面是配置引數
配置檔案增加設定

USER_AUTH_ON 是否需要認證

USER_AUTH_TYPE 認證型別 //1:代表著登入驗證2:代表著試試驗證

USER_AUTH_KEY 認證識別號

REQUIRE_AUTH_MODULE  需要認證模組

NOT_AUTH_MODULE 無需認證模組

USER_AUTH_GATEWAY 認證閘道器 //不是必須的

RBAC_DB_DSN  資料庫連線DSN //不是必須的

RBAC_ROLE_TABLE 角色表名稱

RBAC_USER_TABLE 使用者表名稱 //這裡是使用者和角色的中間表

RBAC_ACCESS_TABLE 許可權表名稱

RBAC_NODE_TABLE 節點表名稱

其中值得注意的是:下面四塊

USER_AUTH_GATEWAY 認證閘道器 //不是必須的

RBAC_DB_DSN  資料庫連線DSN //不是必須的

RBAC_USER_TABLE 使用者表名稱 //這裡是使用者和角色的中間表

USER_AUTH_TYPE 認證型別 //1:代表著登入驗證2:代表著試試驗證

時時驗證是指不將許可權資訊存入session,而是每訪問一個資源,查詢資料庫進行許可權校驗;
登入驗證是指將許可權資訊存入session,而是每訪問一個資源,根據session中的資訊進行驗證;
可以看出時時驗證的安全性更高,但消耗的資源也多。可以看看上面AccessDecision的程式碼加深理解。

下面是配置檔案的程式碼:

<?phpreturn array(

    'RBAC_SUPERADMIN'=>'admin',

    'ADMIN_AUTH_KEY' =>'superAdmin', //超級管理員識別

    'USER_AUTH_ON' =>true,  //是否開啟許可權驗證

    'USER_AUTH_TYPE' =>1,   //驗證型別(1:登入時驗證2:時時驗證)

    'USER_AUTH_KEY' =>'uid', //使用者驗證識別號

    'NOT_AUTH_ACTION' =>'index', // 無需驗證的動作方法

    'NOT_AUTH_MODULE' =>'', //無需驗證的控制器

    'RBAC_ROLE_TABLE' =>'think_role',//角色表名稱

    'RBAC_USER_TABLE' => 'think_role_user',//使用者與角色的中間表

    'RBAC_ACCESS_TABLE' =>'think_access',//許可權表

    'RBAC_NODE_TABLE' =>'think_node',//節點表

    'URL_HTML_SUFFIX' =>'',

    // 'SHOW_PAGE_TRACE' => true,);

開始真正使用RABC類進行許可權驗證
闡述下我用到的兩個類:
LoginController.class.php處理使用者登入請求;
CommonController.class.php新增_initialize()函式,讓其他控制器繼承此類;這裡值得說明的是_initialize()函式,他會在呼叫其他方法之前執行,這也就是為什麼要在這個函式中進行許可權驗證;

One: 使用登入驗證方式:首先在登入的時候向session中寫入許可權LoginController.class.php

<?php/**

 * Created by PhpStorm.

 * Date: 2016/1/29

 * Time: 12:37

 */namespace Admin\Controller;use Org\Util\Rbac;use Think\Controller;class LoginController extends Controller{

    /**

     * 登入首頁檢視

     */

    public function index()

    {

        $this->display();

    }

    /**

     * 登入表單處理

     */

    public function loginHandle()

    {

        if (IS_POST) {

            $userModel = M('user');

            $where = array(

                'phone_num' => I('post.phone_num'),

                'password' => md5(I('post.password'))

            );

            $result = $userModel->where($where)->find();

            if (!$result) {

                $this->error("登陸失敗");

            }else{

                session("phone",I('post.phone_num'));

                session("uid",$result['id']);

                if($result['name']==C('RBAC_SUPERADMIN')){

                    session(C('ADMIN_AUTH_KEY'),true);

                }

                //將許可權寫入session

                Rbac::saveAccessList();

                $this->success('歡迎登陸',U('Admin/Index/index'));

            }

        }

    }

    /**

     * 登出操作

     */

    public function logout(){

        if(session("phone")){

            session("uid",null);

            session("phone",null);

            session(null);

        }

        $this->redirect("Login/index");

    }}

其次,在_initialize()函式中進行許可權驗證:CommonController.class.php

<?php/**

 * Created by PhpStorm.

 * User: zhangpeng

 * Date: 2016/2/1

 * Time: 23:28

 */namespace Admin\Controller;use Org\Util\Rbac;use Think\Controller;/**

 * Class CommonController

 * @package Admin\Controller

 * 使用者驗證使用者的狀態,實現使用者頁面保留的功能

 */class CommonController extends Controller{

    /**

     * 自動執行函式,tp中自帶

     */

    public function _initialize(){

        if(!session("uid")){

            $this->redirect('Login/index');

        }

        if(C('USER_AUTH_ON')){

            Rbac::AccessDecision()||$this->error("你沒有許可權");

        }

    }}

Two:時時驗證,只需要在_initialize()函式中新增驗證即可,程式碼不在贅述。

其中值得注意的是,我用的是TP3.2.3,此時TP支援名稱空間和類自動載入機制,所以不像老版本需要用import引入RBAC類才能使用。

三、總結
RBAC類短短的200多行程式碼實現了許可權控制的需求,感覺十分厲害。後續把我的demo程式碼上傳,與大家一起學習


作者: 小灰灰heart