基于CI的RBAC访问控制

公司内部一直是在用ThinkPHP,最近偶然间接触CI,感觉不错,公司内部使用ThinkPHP用了他们官方示例的RBAC,最近花时间根据CI的一些特性以及ThinkPHP RBAC的基本理念,用CI实现了一套,说是RBAC,其实不只是权限控制,导航菜单的定制以及RBAC的后台页面化管理都已经初步完工了,下面就来看看最初版本。

整个RBAC基本上就是RBAC0的模型,甚至比他更简单,用到的CI钩子-扩展框架的核心。

先看一下RBAC的配置文件,这基本上就是这个的辅助功能了。关于rbac_manage_menu_hidden,rbac_manage_node_hidden这是在使用think的rbac时感觉别扭的地方,RBAC的管理也是根据自身的这套架构来控制的,但是根本没人去再对其进行操作,每次显示在后台特别别扭,所以这里增加两个参数,可以使不想显示在后台的管理节点以及菜单显示。

1
2
3
4
5
6
7
8
$config['rbac_auth_on']	             = TRUE;			//是否开启认证
$config['rbac_auth_type'] = '2'; //认证方式1,登录认证;2,实时认证
$config['rbac_auth_key'] = 'MyAuth'; //SESSION标记
$config['rbac_auth_gateway'] = 'Index/login'; //默认认证网关
$config['rbac_default_index'] = 'manage/Role/index'; //成功登录默认跳转模块
$config['rbac_manage_menu_hidden'] = array('后台管理'); //后台管理导航中不显示的菜单
$config['rbac_manage_node_hidden'] = array('manage'); //后台管理节点中不显示的菜单
$config['rbac_notauth_dirc'] = array(''); //默认无需认证目录array("public","manage")

下面小讲一下代码和原理

config/hooks.php增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$hook['post_controller_constructor'] = array(
'class' => 'Rbac',
'function' => 'aoto_verify',
'filename' => 'rbac_hook.php',
'filepath' => 'hooks',
'params' => '',
);
$hook['display_override'] = array(
'class' => 'Rbac',
'function' => 'view_override',
'filename' => 'rbac_hook.php',
'filepath' => 'hooks',
'params' => '',
);

$hook['pre_system'] = array(
'class' => '',
'function' => 'session_start',
'filename' => '',
'filepath' => '',
'params' => '',
);

post_controller_constructor在你的控制器实例化之后,任何方法调用之前调用权限检测,display_override这个主要是方便显示用的,关于最后的pre_system调用的session_start,整个验证过程都要用到session,而我实在是没有找到好地方调用,只能放在这里了,不知道ci是不是有啥参数之类的能直接开启?

下面是关于aoto_verify的验证的方法,与ThinkPHP是类似的,Think是继承Action自己写的一个Action,以后所有的方法都再集成,既然有了CI的钩子,就不需要那么费劲了,ThinkPHP验证的是分组/模块/方法,在CI中验证的是目录/控制器/方法。为了更方便的取到上面的数据,使用CI的get_instance获取超级对象,然后调用$ci_obj->router就可以了。

aoto_verify()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public function aoto_verify(){
$ci_obj = &get_instance();
//目录
$directory = substr($ci_obj->router->fetch_directory(),0,-1);
//控制器
$controller = $ci_obj->router->fetch_class();
//方法
$function = $ci_obj->router->fetch_method();
//echo "(".$directory."/".$controller."/".$function.")";
if($directory!=""){//当非主目录
if($ci_obj->config->item('rbac_auth_on')){//开启认证
if(!in_array($directory,$ci_obj->config->item('rbac_notauth_dirc'))){//需要验证的目录
//验证是否登录
if(!isset($_SESSION[$ci_obj->config->item('rbac_auth_key')]["INFO"]["id"])){
error_redirct($ci_obj->config->item('rbac_auth_gateway'),"请先登录!");
die();
}
if($ci_obj->config->item('rbac_auth_type')==2){//若为实时认证
$ci_obj->load->model("rbac_model");
//检测用户状态
$STATUS = $ci_obj->rbac_model->check_user_by_id($_SESSION[$ci_obj->config->item('rbac_auth_key')]["INFO"]["id"]);
if($STATUS==FALSE){
error_redirct($this->config->item('rbac_auth_gateway'),$STATUS);
die();
}
//ACL重新赋权
$ci_obj->rbac_model->get_acl($_SESSION[$ci_obj->config->item('rbac_auth_key')]["INFO"]["id"]);
}
//验证ACL权限
if(@!$_SESSION[$ci_obj->config->item('rbac_auth_key')]["ACL"][$directory][$controller][$function]){
error_redirct("","无权访问此节点!(".$directory."/".$controller."/".$function.")");
die();
}
}
}
//已登录且有权限,获取左侧菜单
if($ci_obj->config->item('rbac_auth_type')==2){//若为实时认证
$ci_obj->get_menu = $this->get_menu();
}else{
if(isset($_SESSION[$ci_obj->config->item('rbac_auth_key')]["MENU"])){
$ci_obj->get_menu = $_SESSION[$ci_obj->config->item('rbac_auth_key')]["MENU"];
}else{
$_SESSION[$ci_obj->config->item('rbac_auth_key')]["MENU"] = $this->get_menu();
$ci_obj->get_menu = $_SESSION[$ci_obj->config->item('rbac_auth_key')]["MENU"];
}
}
//默认重写View开
$ci_obj->view_override = TRUE;
}
}

PS:在这里只对controllers中的二级目录做了权限控制,一级没有。在上述方法后,还有一句$this->get_menu(),这里是获取左侧的导航菜单数据。

get_menu()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$ci_obj = &get_instance();
$ci_obj->load->database();
$query = $ci_obj->db->query("SELECT rm.id,rm.title,rm.node_id,rm.p_id,rn.dirc,rn.cont,rn.func FROM rbac_menu rm left join rbac_node rn on rm.node_id = rn.id WHERE rm.status = 1 AND rm.p_id is NULL ORDER BY sort asc");
$menu_data = $query->result();
$i = 0;
while(count($menu_data)>0){
$id_list = "";
foreach($menu_data as $vo){
if($i==2){
$vo->p_p_id = $Tmp_menu[1][$vo->p_id]->p_id;
}
$Tmp_menu[$i][$vo->id] = $vo;
$id_list .= $vo->id.",";
}
$id_list = substr($id_list,0,-1);
$query = $ci_obj->db->query("SELECT rm.id,rm.title,rm.node_id,rm.p_id,rn.dirc,rn.cont,rn.func FROM rbac_menu rm left join rbac_node rn on rm.node_id = rn.id WHERE rm.status = 1 AND rm.p_id in (".$id_list.") ORDER BY sort asc");
$menu_data = $query->result();
$i++;
}
$j = 0;
//按权限进行展示
foreach($Tmp_menu as $vo){
foreach($vo as $cvo){
if(@$_SESSION[$ci_obj->config->item('rbac_auth_key')]["ACL"][$cvo->dirc][$cvo->cont][$cvo->func]||!$cvo->node_id){
if($j==0){
if(@$_SESSION[$ci_obj->config->item('rbac_auth_key')]["ACL"][$cvo->dirc][$cvo->cont][$cvo->func]){
$menu[$cvo->id]["shown"] = 1;
}
$menu[$cvo->id]["self"] = array("title"=>$cvo->title,"uri"=>$cvo->dirc?$cvo->dirc."/".$cvo->cont."/".$cvo->func:$cvo->cont."/".$cvo->func);

}elseif($j==1){
if(@$_SESSION[$ci_obj->config->item('rbac_auth_key')]["ACL"][$cvo->dirc][$cvo->cont][$cvo->func]){
$menu[$cvo->p_id]["shown"] = 1;
$menu[$cvo->p_id]["child"][$cvo->id]["shown"] = 1;
}
$menu[$cvo->p_id]["child"][$cvo->id]["self"] = array("title"=>$cvo->title,"uri"=>$cvo->dirc?$cvo->dirc."/".$cvo->cont."/".$cvo->func:$cvo->cont."/".$cvo->func);

}else{
if(@$_SESSION[$ci_obj->config->item('rbac_auth_key')]["ACL"][$cvo->dirc][$cvo->cont][$cvo->func]){
$menu[$cvo->p_p_id]["shown"] = 1;
$menu[$cvo->p_p_id]["child"][$cvo->p_id]["shown"] = 1;
$menu[$cvo->p_p_id]["child"][$cvo->p_id]["child"][$cvo->id]["shown"] = 1;
}
$menu[$cvo->p_p_id]["child"][$cvo->p_id]["child"][$cvo->id]["self"] = array("title"=>$cvo->title,"uri"=>$cvo->dirc?$cvo->dirc."/".$cvo->cont."/".$cvo->func:$cvo->cont."/".$cvo->func);
}
}
}
$j++;
}
return $menu;

关于这个方法其实就是数组的拼接以及是否显示的验证。

关于数据库,一共5张表,4张表实现权限的控制,1张表主要是左侧的菜单,各表之间的关系还是比较明了简洁的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

CREATE TABLE IF NOT EXISTS `rbac_auth` (
`node_id` int(11) NOT NULL COMMENT '节点ID',
`role_id` int(11) NOT NULL COMMENT '角色ID',
UNIQUE KEY `nid_rid` (`node_id`,`role_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='角色与节点对应表';

CREATE TABLE IF NOT EXISTS `rbac_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(20) NOT NULL COMMENT '导航名称',
`node_id` int(11) DEFAULT NULL COMMENT '节点ID',
`p_id` int(11) DEFAULT NULL COMMENT '导航父id',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` int(11) DEFAULT '1' COMMENT '状态(1:正常,0:停用)',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='菜单表' AUTO_INCREMENT=20 ;

CREATE TABLE IF NOT EXISTS `rbac_node` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dirc` varchar(20) NOT NULL COMMENT '目录',
`cont` varchar(10) NOT NULL COMMENT '控制器',
`func` varchar(10) NOT NULL COMMENT '方法',
`memo` varchar(25) DEFAULT NULL COMMENT '备注',
`status` int(11) NOT NULL DEFAULT '1' COMMENT '状态(1:正常,0:停用)',
PRIMARY KEY (`id`),
UNIQUE KEY `d_c_f` (`dirc`,`cont`,`func`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='节点表' AUTO_INCREMENT=24 ;

CREATE TABLE IF NOT EXISTS `rbac_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rolename` varchar(25) NOT NULL COMMENT '角色名',
`status` int(11) NOT NULL DEFAULT '1' COMMENT '状态(1:正常,0停用)',
PRIMARY KEY (`id`),
UNIQUE KEY `rolename` (`rolename`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='角色表' AUTO_INCREMENT=4 ;

CREATE TABLE IF NOT EXISTS `rbac_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(32) NOT NULL COMMENT '密码',
`nickname` varchar(20) NOT NULL COMMENT '昵称',
`email` varchar(25) NOT NULL COMMENT 'Email',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`status` int(11) NOT NULL DEFAULT '1' COMMENT '状态(1:正常,0:停用)',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='用户表' AUTO_INCREMENT=6 ;

OK,CI刚刚接触,可能有些地方使用不当,这个在后期在进行改版。 PS这里我用的CI版本为2.1.4

样式使用的bootstrap3.0,

GITHUB:https://github.com/toryzen/CI_RBAC