onethink權限管理主要分為兩個方面1種菜單節(jié)點檢測,另外一種是動態(tài)檢測(未實現(xiàn))。
第1次進入系統(tǒng)后,在Admin/Controller/AdminController.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();//動態(tài)檢測的代碼,返回null
if( $dynamic === null ){
//檢測非動態(tài)權限
$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的數(shù)據(jù)庫中有4張表是和權限管理有關聯(lián)的,
其中rule表對應的是此系統(tǒng)中所有的url生成的規(guī)則表,group表對應的是某個分組所具有的權限,也就是某個分組可以訪問的url集合。group_access代表的某個用戶屬于某個組,extend表主要用來實現(xiàn)動態(tài)檢測。
在/Admin/Controller/AdminController.class.php中進行的第1次權限檢測,
/**
* action訪問控制,在 **登陸成功** 后履行的第1項權限檢測任務
*
* @return boolean|null 返回值必須使用 `===` 進行判斷
*
* 返回 **false**, 不允許任何人訪問(超管除外)
* 返回 **true**, 允許任何管理員訪問,無需履行節(jié)點權限檢測
* 返回 **null**, 需要繼續(xù)履行節(jié)點權限檢測決定是不是允許訪問
*
*/
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;//需要檢測節(jié)點權限
}
權限認證的配置在/ThinkPHP/Library/Think/Auth.class.php中如圖:
規(guī)則驗證中最重要的函數(shù)為check()函數(shù):
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) { #如果是多個,將其拆分成數(shù)組
$name = explode(',', $name);
} else {
$name = array($name);
}
}
$list = array(); //保存驗證通過的規(guī)則名
if ($mode=='url') {
$REQUEST = unserialize( strtolower(serialize($_REQUEST)) );
}
foreach ( $authList as $auth ) {
$query = preg_replace('/^.+\?/U','',$auth);#取得參數(shù)字符串
if ($mode=='url' && $query!=$auth ) {
parse_str($query,$param); //解析規(guī)則中的param 生成1個數(shù)組,鍵值對對應url中的鍵值對
$intersect = array_intersect_assoc($REQUEST,$param);#輸出$REQUEST 和$param的交集
$auth = preg_replace('/\?.*$/U','',$auth);#此時的$auth為url路徑
if ( in_array($auth,$name) && $intersect==$param ) { //如果節(jié)點符合且url參數(shù)滿足
$list[] = $auth ;
}
}else if (in_array($auth , $name)){#遍歷用戶具有的權限數(shù)組,如果某個權限存在于$name數(shù)組中,則將其放入$list數(shù)組,假定用戶具有權限為1,2,3,4,5,
#需要驗證的權限為2,6.那末會將2放入$list數(shù)組,
$list[] = $auth ;
}
}
exit;
if ($relation == 'or' and !empty($list)) {#如上個例子中,當為或時,只要$list數(shù)組不為空,既只要滿足1個權限就能夠
return true;
}
$diff = array_diff($name, $list);
if ($relation == 'and' and empty($diff)) {#如上例中,當為與時,需要滿足$List數(shù)組和$name數(shù)組完全相同才可以,既$name中的權限全部存在于$auth中
return true;
}
return false;
}
由于后臺的控制器都繼承了AdminController控制器,所以每打開1個url,都會首先檢測改用戶是不是具有權限。
進入后臺后,進入到用戶的權限管理頁面,如默許用戶組,履行的方法為:
public function access(){
$this->updateRules();//首先履行此方法,此方法根據(jù)menu表中的數(shù)據(jù)更新rule表中的數(shù)據(jù),具體見下方代碼
$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表,取得主菜單數(shù)組和子菜單數(shù)組
$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(){
//需要新增的節(jié)點必定位于$nodes
$nodes = $this->returnNodes(false); #returnNodes查詢出表menu中的所有菜單項,生成1個2維數(shù)組,其中的1個值以下:
/* 0 => array:4 [▼
* "title" => "文檔列表"
* "url" => "Admin/article/index"
*"tip" => ""
*"pid" => "2"
* ]
*/
$AuthRule = M('AuthRule');
$map = array('module'=>'admin','type'=>array('in','1,2'));
//需要更新和刪除的節(jié)點必定位于$rules
$rules = $AuthRule->where($map)->order('name')->select();//查詢出屬于admin模塊的所有規(guī)則,其中type=1代表url,type=2代表主菜單
//構建insert數(shù)據(jù)
$data = array();//保存需要插入和更新的新節(jié)點
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的1個子數(shù)組以下:此時$data存儲的為menu表中的數(shù)據(jù)
* "admin/article/indexadmin1" => array:5 [▼
* "name" => "Admin/article/index"
* "title" => "文檔列表"
* "module" => "admin"
* "type" => 1
* "status" => 1
]
*/
$update = array();//保存需要更新的節(jié)點
$ids = array();//保存需要刪除的節(jié)點的id
foreach ($rules as $index=>$rule){//$data是菜單生成的數(shù)組,此循環(huán)的作用是根據(jù)菜單數(shù)組,來進行規(guī)則表的增刪改操作,如果規(guī)則數(shù)組中的某個鍵和菜單數(shù)組的鍵相同則將菜單數(shù)組
//中的該值放入$updata表,將規(guī)則數(shù)組的值放入$diff表,如果規(guī)則數(shù)組中某個值不存在與菜單數(shù)組中,說明規(guī)則數(shù)組中的該值需要刪除
$key = strtolower($rule['name'].$rule['module'].$rule['type']);
if ( isset($data[$key]) ) {//如果數(shù)據(jù)庫中的規(guī)則與配置的節(jié)點匹配,說明是需要更新的節(jié)點
$data[$key]['id'] = $rule['id'];//為需要更新的節(jié)點補充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是規(guī)則表生成的
foreach ($update as $k=>$row){
if ( $row!=$diff[$row['id']] ) {//判斷菜單數(shù)組的數(shù)據(jù)是不是有更新,如果有更新,規(guī)則表也進行更新
$AuthRule->where(array('id'=>$row['id']))->save($row);
}
}
}
if ( count($ids) ) { //
$AuthRule->where( array( 'id'=>array('IN',implode(',',$ids)) ) )->save(array('status'=>-1));
//刪除規(guī)則是不是需要從每一個用戶組的訪問授權表中移除該規(guī)則?
}
//需要更新的$data已unset掉,剩余的數(shù)據(jù)為為新增數(shù)據(jù),履行add操作
if( count($data) ){
$AuthRule->addAll(array_values($data));//array_values函數(shù)將關聯(lián)數(shù)組變成索引數(shù)組,只作用的1維
}
if ( $AuthRule->getDbError() ) {
trace('['.__METHOD__.']:'.$AuthRule->getDbError());
return false;
}else{
return true;
}
}
生成菜單數(shù)據(jù)后,view層使用3層循環(huán)將數(shù)據(jù)輸出,循環(huán)的數(shù)據(jù)如內容
<volist name="node_list" id="node" >//第1次循環(huán)主菜單
<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" > //第2次循環(huán)子菜單
<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']">
<span class="child_row">
<volist name="child['operator']" id="op"> //第3次循環(huán)操作
<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>
</span>
</notempty>
</div>
</volist>
</present>
</dd>
</dl>
</volist>
對如何生成菜單數(shù)據(jù)主要調用了兩個函數(shù)為:returnNodes()和函數(shù)list_to_tree(),
returnNodes()函數(shù)的代碼為:
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數(shù)組的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{//返回1維數(shù)組
$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()函數(shù)的代碼為:
function list_to_tree($list, $pk='id', $pid = 'pid', $child = '_child', $root = 0) {
// 創(chuàng)建Tree
$tree = array();
if(is_array($list)) {
// 創(chuàng)建基于主鍵的數(shù)組援用
$refer = array();
foreach ($list as $key => $data) {
$refer[$data[$pk]] = & $list[$key];//將$list數(shù)組以援用的方式轉換成$refer數(shù)組,鍵為子數(shù)組的id值
}
foreach ($list as $key => $data) {
// 判斷是不是存在parent
$parentId = $data[$pid];
if ($root == $parentId) {//此時pid = 0為主菜單,直接放入$tree數(shù)組
$tree[] =& $list[$key];
}else{
if (isset($refer[$parentId])) {//此時當前url的父菜單在$refer中
$parent =& $refer[$parentId];
$parent[$child][] =& $list[$key];
// dump($parent);
}
}
}
}
return $tree;
}
函數(shù)list_to_tree()僅使用的幾行代碼就生成了1個樹,現(xiàn)分析以下 :
$parent =& $refer[$parentId]是以援用的方式賦值,所以改變$parent的值,就相當于改變$refer的值,又由于 $refer[$data[$pk]] = $list[$key], 所以改變$refer的值就相當于改變$list的值,又由于$tree[] =& $list[$key]所以改變$list的值就相當于改變$tree的值,總結為:改變了$parent的值就相當于改變了$tree的值,以上圖為例,它是生成的樹形結構中的用戶分類,當遍歷到用戶信息時,在$refer中含有用戶這個數(shù)組,所以會在用戶這個數(shù)組中添加1個子元素,鍵為operator,值為用戶信息這個數(shù)組,當遍歷到新增用戶時,一樣查找$refer,在$refer這個數(shù)組中含有用戶信息這個數(shù)組,所以給用戶信息這個數(shù)組添加1個子元素,鍵為operator,值為新增用戶這個數(shù)組,由于使用援用的關系,所以$tree數(shù)組的每個元素都是到此函數(shù)履行到最后1步才肯定的,比如當用戶信息添加了子元素新增用戶時,用戶這個數(shù)組也會隨著進行變動。