onethink权限管理主要分为两个方面一种菜单节点检测,另一种是动态检测(未实现)。
第一次进入系统后,在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();//动态检测的代码,返回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="">{$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=""/>{$child.title}
label>
div>
<notempty name="child['operator']">
<span class="child_row">
<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=""/>{$op.title}
label>
volist>
span>
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数组的每一个元素都是到此函数执行到最后一步才确定的,比如当用户信息添加了子元素新增用户时,用户这个数组也会跟着进行变动。