onethink權限管理主要分為兩個方面一種菜單節點檢測,另一種是動態檢測(未實現)。
第一次進入系統后,在Admin/Controller/AdminController.html' target='_blank'>class.php中權限驗證的代碼為:
define('IS_ROOT', is_administrator()); if(!IS_ROOT && C('ADMIN_ALLOW_IP')){ // 檢查IP地址訪問 if(!in_array(get_client_ip(),explode(',',C('ADMIN_ALLOW_IP')))){ $this->error('403:禁止訪問'); } } $access = $this->accessControl(); if ( $access === false ) { $this->error('403:禁止訪問'); }elseif( $access === null ){ $dynamic = $this->checkDynamic();//動態檢測的代碼,返回null if( $dynamic === null ){ //檢測非動態權限 $rule = strtolower(MODULE_NAME.'/'.CONTROLLER_NAME.'/'.ACTION_NAME); if(!IS_ROOT) { if (!$this->checkRule($rule, array('in', '1,2'))) { $this->error('未授權訪問!'); exit; } } }elseif( $dynamic === false ){ $this->error('未授權訪問!'); } }
在onethink的數據庫中有四張表是和權限管理有關聯的,
其中rule表對應的是此系統中所有的url生成的規則表,group表對應的是某個分組所擁有的權限,也就是某個分組可以訪問的url集合。group_access代表的某個用戶屬于某個組,extend表主要用來實現動態檢測。
在/Admin/Controller/AdminController.class.php中進行的第一次權限檢測,
/** * action訪問控制,在 **登陸成功** 后執行的第一項權限檢測任務 * * @return boolean|null 返回值必須使用 `===` 進行判斷 * * 返回 **false**, 不允許任何人訪問(超管除外) * 返回 **true**, 允許任何管理員訪問,無需執行節點權限檢測 * 返回 **null**, 需要繼續執行節點權限檢測決定是否允許訪問 * */ final protected function accessControl(){ $allow = C('ALLOW_VISIT'); $deny = C('DENY_VISIT');#這兩項配置存儲在config表中 $check = strtolower(CONTROLLER_NAME.'/'.ACTION_NAME); if ( !empty($deny) && in_array_case($check,$deny) ) { return false;//非超管禁止訪問deny中的方法 } if ( !empty($allow) && in_array_case($check,$allow) ) { return true; } return null;//需要檢測節點權限 }
權限認證的配置在/ThinkPHP/Library/Think/Auth.class.php中如圖:
規則驗證中最重要的函數為check()函數:
public function check($name, $uid, $type=1, $mode='url', $relation='or') { if (!$this->_config['AUTH_ON'])#如果沒有開啟驗證,返回true return true; $authList = $this->getAuthList($uid,$type); //獲取用戶擁有的權限列表 if (is_string($name)) { $name = strtolower($name); if (strpos($name, ',') !== false) { #如果是多個,將其拆分成數組 $name = explode(',', $name); } else { $name = array($name); } } $list = array(); //保存驗證通過的規則名 if ($mode=='url') { $REQUEST = unserialize( strtolower(serialize($_REQUEST)) ); } foreach ( $authList as $auth ) { $query = preg_replace('/^.+?/U','',$auth);#獲得參數字符串 if ($mode=='url' && $query!=$auth ) { parse_str($query,$param); //解析規則中的param 生成一個數組,鍵值對對應url中的鍵值對 $intersect = array_intersect_assoc($REQUEST,$param);#輸出$REQUEST 和$param的交集 $auth = preg_replace('/?.*$/U','',$auth);#此時的$auth為url路徑 if ( in_array($auth,$name) && $intersect==$param ) { //如果節點相符且url參數滿足 $list[] = $auth ; } }else if (in_array($auth , $name)){#遍歷用戶擁有的權限數組,如果某個權限存在于$name數組中,則將其放入$list數組,假設用戶擁有權限為1,2,3,4,5, #需要驗證的權限為2,6.那么會將2放入$list數組, $list[] = $auth ; } } exit; if ($relation == 'or' and !empty($list)) {#如上個例子中,當為或時,只要$list數組不為空,既只要滿足一個權限就可以 return true; } $diff = array_diff($name, $list); if ($relation == 'and' and empty($diff)) {#如上例中,當為與時,需要滿足$List數組和$name數組完全相同才可以,既$name中的權限全部存在于$auth中 return true; } return false; }
因為后臺的控制器都繼承了AdminController控制器,所以每打開一個url,都會首先檢測改用戶是否具有權限。
進入后臺后,進入到用戶的權限管理頁面,如默認用戶組,執行的方法為:
public function access(){ $this->updateRules();//首先執行此方法,此方法根據menu表中的數據更新rule表中的數據,具體見下方代碼 $auth_group = M('AuthGroup')->where( array('status'=>array('egt','0'),'module'=>'admin','type'=>AuthGroupModel::TYPE_ADMIN) ) ->getfield('id,id,title,rules'); $node_list = $this->returnNodes();//查詢menu表,獲得主菜單數組以及子菜單數組 $map = array('module'=>'admin','type'=>AuthRuleModel::RULE_MAIN,'status'=>1); $main_rules = M('AuthRule')->where($map)->getField('name,id');//查詢rule表獲得主菜單的url和id值 $map = array('module'=>'admin','type'=>AuthRuleModel::RULE_URL,'status'=>1); $child_rules = M('AuthRule')->where($map)->getField('name,id');//查詢rule表獲得子菜單的url和id值 $this->assign('main_rules', $main_rules); $this->assign('auth_rules', $child_rules); $this->assign('node_list', $node_list); $this->assign('auth_group', $auth_group); $this->assign('this_group', $auth_group[(int)$_GET['group_id']]);//當前用戶組 $this->meta_title = '訪問授權'; $this->display('managergroup'); }
public function updateRules(){ //需要新增的節點必然位于$nodes $nodes = $this->returnNodes(false); #returnNodes查詢出表menu中的所有菜單項,生成一個二維數組,其中的一個值如下: /* 0 => array:4 [▼ * 'title' => '文檔列表' * 'url' => 'Admin/article/index' *'tip' => '' *'pid' => '2' * ] */ $AuthRule = M('AuthRule'); $map = array('module'=>'admin','type'=>array('in','1,2')); //需要更新和刪除的節點必然位于$rules $rules = $AuthRule->where($map)->order('name')->select();//查詢出屬于admin模塊的所有規則,其中type=1代表url,type=2代表主菜單 //構建insert數據 $data = array();//保存需要插入和更新的新節點 foreach ($nodes as $value){ $temp['name'] = $value['url']; $temp['title'] = $value['title']; $temp['module'] = 'admin'; if($value['pid'] >0){ $temp['type'] = AuthRuleModel::RULE_URL;//RULE_URL為1代表url }else{ $temp['type'] = AuthRuleModel::RULE_MAIN;//RULE_MAIN為2代表主菜單 } $temp['status'] = 1; $data[strtolower($temp['name'].$temp['module'].$temp['type'])] = $temp;//去除重復項 } /*$data的一個子數組如下:此時$data存儲的為menu表中的數據 * 'admin/article/indexadmin1' => array:5 [▼ * 'name' => 'Admin/article/index' * 'title' => '文檔列表' * 'module' => 'admin' * 'type' => 1 * 'status' => 1 ] */ $update = array();//保存需要更新的節點 $ids = array();//保存需要刪除的節點的id foreach ($rules as $index=>$rule){//$data是菜單生成的數組,此循環的作用是根據菜單數組,來進行規則表的增刪改操作,如果規則數組中的某個鍵和菜單數組的鍵相同則將菜單數組 //中的該值放入$updata表,將規則數組的值放入$diff表,如果規則數組中某個值不存在與菜單數組中,說明規則數組中的該值需要刪除 $key = strtolower($rule['name'].$rule['module'].$rule['type']); if ( isset($data[$key]) ) {//如果數據庫中的規則與配置的節點匹配,說明是需要更新的節點 $data[$key]['id'] = $rule['id'];//為需要更新的節點補充id值 $update[] = $data[$key]; unset($data[$key]); unset($rules[$index]); unset($rule['condition']); $diff[$rule['id']]=$rule; }elseif($rule['status']==1){ $ids[] = $rule['id']; } } if ( count($update) ) { //$update是菜單表生成的,$diff是規則表生成的 foreach ($update as $k=>$row){ if ( $row!=$diff[$row['id']] ) {//判斷菜單數組的數據是否有更新,如果有更新,規則表也進行更新 $AuthRule->where(array('id'=>$row['id']))->save($row); } } } if ( count($ids) ) { // $AuthRule->where( array( 'id'=>array('IN',implode(',',$ids)) ) )->save(array('status'=>-1)); //刪除規則是否需要從每個用戶組的訪問授權表中移除該規則? } //需要更新的$data已經unset掉,剩余的數據為為新增數據,執行add操作 if( count($data) ){ $AuthRule->addAll(array_values($data));//array_values函數將關聯數組變為索引數組,只作用的一維 } if ( $AuthRule->getDbError() ) { trace('['.__METHOD__.']:'.$AuthRule->getDbError()); return false; }else{ return true; } }
生成菜單數據后,view層使用三層循環將數據輸出,循環的數據如內容
<volist name='node_list' id='node' >//第一次循環主菜單 <dl class='checkmod'> <dt class='hd'> <label class='checkbox'><input class='auth_rules rules_all' type='checkbox' name='rules[]' value='<?php echo $main_rules[$node['url']] ?>'>{$node.title}管理</label> </dt> <dd class='bd'> <present name='node['child']'> <volist name='node['child']' id='child' > //第二次循環子菜單 <div class='rule_check'> <div> <label class='checkbox' <notempty name='child['tip']'>title='{$child.tip}'</notempty>> <input class='auth_rules rules_row' type='checkbox' name='rules[]' value='<?php echo $auth_rules[$child['url']] ?>'/>{$child.title} </label> </div> <notempty name='child['operator']'> <volist name='child['operator']' id='op'> //第三次循環操作 <label class='checkbox' <notempty name='op['tip']'>title='{$op.tip}'</notempty>> <input class='auth_rules' type='checkbox' name='rules[]' value='<?php echo $auth_rules[$op['url']] ?>'/>{$op.title} </label> </volist> </notempty> </div> </volist> </present> </dd> </dl> </volist>
對于如何生成菜單數據主要調用了兩個函數為:returnNodes()和函數list_to_tree(),
returnNodes()函數的代碼為:
final protected function returnNodes($tree = true){ static $tree_nodes = array(); if ( $tree && !empty($tree_nodes[(int)$tree]) ) { return $tree_nodes[$tree]; } if((int)$tree){ $list = M('Menu')->field('id,pid,title,url,tip,hide')->order('sort asc')->select(); foreach ($list as $key => $value) { //給$list數組的url字段加上模塊名 if( stripos($value['url'],MODULE_NAME)!==0 ){ $list[$key]['url'] = MODULE_NAME.'/'.$value['url']; } } $nodes = list_to_tree($list,$pk='id',$pid='pid',$child='operator',$root=0);//將菜單生成樹形結構 foreach ($nodes as $key => $value) { if(!empty($value['operator'])){ $nodes[$key]['child'] = $value['operator'];//將鍵名由operator更改為child unset($nodes[$key]['operator']); } } }else{//返回一維數組 $nodes = M('Menu')->field('title,url,tip,pid')->order('sort asc')->select(); foreach ($nodes as $key => $value) { if( stripos($value['url'],MODULE_NAME)!==0 ){ $nodes[$key]['url'] = MODULE_NAME.'/'.$value['url']; } } } $tree_nodes[(int)$tree] = $nodes; return $nodes; }
list_to_tree()函數的代碼為:
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];//將$list數組以引用的方式轉換成$refer數組,鍵為子數組的id值 } foreach ($list as $key => $data) { // 判斷是否存在parent $parentId = $data[$pid]; if ($root == $parentId) {//此時pid = 0為主菜單,直接放入$tree數組 $tree[] =& $list[$key]; }else{ if (isset($refer[$parentId])) {//此時當前url的父菜單在$refer中 $parent =& $refer[$parentId]; $parent[$child][] =& $list[$key];// dump($parent); } } } } return $tree;}
函數list_to_tree()僅使用的幾行代碼就生成了一個樹,現分析如下 :
$parent =& $refer[$parentId]是以引用的方式賦值,所以改變$parent的值,就相當于改變$refer的值,又因為 $refer[$data[$pk]] = $list[$key], 所以改變$refer的值就相當于改變$list的值,又因為$tree[] =& $list[$key]所以改變$list的值就相當于改變$tree的值,總結為:改變了$parent的值就相當于改變了$tree的值,以上圖為例,它是生成的樹形結構中的用戶分類,當遍歷到用戶信息時,在$refer中含有用戶這個數組,所以會在用戶這個數組中添加一個子元素,鍵為operator,值為用戶信息這個數組,當遍歷到新增用戶時,同樣查找$refer,在$refer這個數組中含有用戶信息這個數組,所以給用戶信息這個數組添加一個子元素,鍵為operator,值為新增用戶這個數組,因為使用引用的關系,所以$tree數組的每一個元素都是到此函數執行到最后一步才確定的,比如當用戶信息添加了子元素新增用戶時,用戶這個數組也會跟著進行變動。
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。
新聞熱點
疑難解答