基于ThinkPHP的RBAC权限管理的设计与实现

2018-01-14 12:35谢飞翔黄谦
活力 2018年18期

谢飞翔 黄谦

【摘要】权限认证作为信息管理系统中必要的重要组成部分,它不仅要提供安全、可靠、高效的权限认证,而且应具备很好的适应性和灵活性以满足不同信息系统的需求。本文正是基于以上理念,给出了一个通用RBAC数据库设计方案,并以ThinkPHP框架为开发环境,着重介绍了系统权限的设置、分配和验证。

【关键词】权限控制:权限验证;RBAC;ThinkPHP

引言

权限认证模块在任何一个信息管理系统中都是不可或缺的,能有效防止非法用户访问系统资源以及合法用户越权使用系统资源。

目前主要的访问控制技术有自主访问控制、强制访问控制和基于角色的访问控制等,从优越性上看,RBAC优于前两者,RBAC是基于角色的访问控制,通过角色可以很好地把权限和用户独立开来,因此RBAC是目前信息系统的主流设计模型,尤其被广泛应用于Web信息管理系统。

本文以RBAC模型为核心理念,基于TinkPHP优越的框架底层,实现了Web环境下,以角色为基础的权限控制功能。

一、RBAC模型简介

简单而言,RBAC核心思想是提出了角色(Role)的概念,把用户与权限相分离。主要包括三个要素,用户、角色和权限。权限与角色相关联,用户通过成为适当角色的成员而得到这些角色对应的权限。这就极大地简化了权限的管理。角色可依据新的需求被赋予新的权限,而权限也可根据需要而从某角色中回收。一个用户可以根据需要被指派多个角色以适应更为灵活的系统需求。

本文所采用的RBAC模型包括用户(USERS)、角色( ROIES)和权限(PRMS)等3类实体集合。三者之间的逻辑关系如图1所示。

每个角色至少具备一个权限,每个用户至少扮演一个角色;可以对两个完全不同的角色分配完全相同的访问权限;需要强调的是,将用户和许可权进行分离,使得彼此相互独立,它们之间是多对多的关系,这种关系将使得模型具备足够的灵活性以适应不同的系统需求。如,企业副总经理兼任财务总监,该用户将一身兼两职,也将同时拥有两个角色的权限。

二、RBAC数据库设计

RBAC就是用户通过角色与权限进行关联,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户一角色一权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般是多对多的关系。基于以上RBAC思想,给出图2所示的数据库设计概要,鉴于突出问题重点,省去了表中的一些通用业务字段。

用户表与角色表之间的关系是多对多的关系,中间表为用户角色分配表,一个用户可以指定多个角色,一个角色也可以同时分配给多个用户;角色表与权限表之间的关系也是多对多的关系,中间表为角色权限分配表,一个角色可以指定拥有多个权限,一个权限也可以同时分配给多个角色。

三、RBAC系统功能的实现

ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架,是为了敏捷WEB应用开发和简化企业应用开发而诞生的。本文拟基于ThinkPHP实现RBAC的权限管理、用户管理、角色管理、用户授权及认证等功能。

(一)RBAC权限的设置和分配

在数据库设计中,我们知道程序中核心的数据库表有用户表( user)、角色表(role)、用户角色分配表(role_user)、权限表(node)、角色权限分配表(access)等5张表。以下重点讨论,程序怎样实现系统权限的设置和分配的。

1. RBAC权限的设置

权限的设置是基于系统权限表(node),表结构及样例如图3所示。

可见,所有权限节点都有一个pid值,意为其父亲节点id,如果pid-0,表示当前节点为模块级权限,同时对应level-1,意为一级根节点,所有当前模块下授权控制器和方法都会隶属于该根节点;每一个授权控制器对应一个pid-l/level-2/type-1的二级节点,意为隶属xfadmin模块的二级控制器节点;每一个授权控制器方法对应一个pid-n/level-3/type-0的三级节点,意为隶属n号控制器的三级方法节点,如,29号节点是隶属于“用户管理”控制器的“添加”方法节点。通过pid、level、type三个字段的合理设置,所有权限节点就构成了一棵权限树。说明:表中 group_id并非rbac的要素字段,该字段设置的目的是对所有节点功能进行分组,便于在前台界面进行展示,group_id=0,表示未分组,该权限只作为功能点存在,并不会在功能界面展示;groupid>0,表示进行了分组,该权限会以独立的功能点展示在前台界面中。

name和title是权限的名称和标题,name属性对应具体程序的模块名、控制器名。如,34号结点是xfadmin模块下的AdminUser控制器下的password方法。title字段主要是前台展示时的标记名。

系统权限管理功能主要有:权限添加、权限删除、权限编辑、权限禁用与恢复等几个核心操作;权限节点分为一级模块节点,如“后台管理”,二级控制器节点,如“節点管理”,三级方法节点,如“添加”。

下面重点看看“添加”权限功能,其实现代码是AdminNode\add方法,下面重点看看“添加”权限功能,其实现代码是AdminNo de\add方法,对应源码片段为:

public function add() {

$controller - $this->request->controller();

if ($this->request->isAjax() {

$data - $this->request->except(['id']);

if (

class exists($validateClass = Loader: :parseClass(Config: :get(app.validate_path'), 'validate'. $controller))) {

$validate - new $validateClass();

if (!$validate->check($data)) {return ajax_return_adv_error($validate->getError));}

}

if (

classexists($mo deIClass - Loader: :parseClass(Config : :get(' app.mo del_path'), 'model'. $this->parseCameICase($controller)))

||classexists($modeIClass=Loader::parseClass(Config::get(app.model_path'), 'model', $controller))) {

$model=new $modeIClass();

$ret=$model->isUpdate(false)->save($data);

} else {

Db : :startTrans();

try {

$model=Db::name($this->parseTable($controller));

$ret=$model->insert($data);

Db: :commit();

} catch(\Exception $e) {

Db::rollback();

return ajax_return_adv_error($e->getMessage());

}

}

return ajax_return_adv('success');

} else {

return $this->view->fetch(isset($this->template) ?$this->template:'edit');

}

}

代码分析:由以上代码分析可知,该add方法实际上一个通用添加方法,控制器名称对应数据库表名和验证器名称,用户在访问控制器时,程序会自动调用对应的验证器和数据库表进行数据处理。另外,可以针对不同的控制器在模型库中定义不同的模型,充分体现了程序设计扩展性的优越性。

2. RBAC权限的分配

权限设置完之后,就需要基于用户表(user)、用户角色表(roleuser)、角色表(role)、角色权限表(access)将权限分配给用户。其邏辑是先基于“角色权限表”将权限分配给角色,然后再基于“用户角色表”将角色分配给用户,进而完成用户权限的授权。

(1)角色授权

角色表包括的核心字段有id和name,角色权限表包括核心字段有role id、node id,其中 role id对应角色表id,node id对应node表id;一个角色分配了一个权限就会生成一条角色权限表记录,一个角色可以分配若干个权限,同一个权限可以被分配给多个角色。

角色的授权功能在角色管理模块中实现,通过过添加功能创建角色后,可以立即通过授权功能对其进行权限分配,系统权限会以树型的方式提供给用户进行选择分配,操作界面如图4所示。

以上所有功能都是通过AdminRole控制器来实现的,其中角色授权是通过AdminRole\access方法实现的,access方法源码为:

public function access(){

$roleid=$this->request->param('id/d');

if ($this->request->isPost()){

if(! $role_id){return ajax_return adv error(''缺少

必要参数'');}

if (

true !==$error=Loader::modeI('AdminAccess', 'Iogic')->insertAccess($role_id, $this->request->post()》

{return ajax_return_adv_error($error); }

retum ajax_return_adv("权限分配成功", ");

} else {

if (!$roleid) {throw new Exception("缺少必要参

$tree=Loader::model('AdminRole','Iogic')->getAccessTree($role_id);

$this->view->assign("tree", json_encode($tree));

return $this->view->fetch();

}

}

代码分析:用户执行授权时,程序首先会执行else部分,程序获取请求的参数是角色id,然后再通过logic\AdminRole功能类的get Access Tree方法拿到当前角色的权限集,该权限是根据用户id(UID)查询出的当前用户所拥有的权限集,也即用户只能基于自己拥有的权限进行再分配,这个功能是很多企业所需要的。

当用户分配完权限后通过保存功能,执行角色权限的保存,其实就是执行access方法的$this->request->isPost()条件为真的部分,代码中通过logic\Admin Access功能类的rnsertAccess方法完成角色权限的保存,insertAccess参数错误

public function insertAccess($role_id, $data){

Db::startTrans();

try{

$db access=Db::name("AdminAccess");

$db access->where("role_id". $role_id)->delete0;

if (isset($data['node_id'])&&!empty($data['node_id'])&&isarray($data['nodeid'])){

$insert_all=[];

foreach($data['nodeid']as$v){

$node=explode(‘_,$v);

if(!isset($node[2])){throw new Exception('参数错误);}

$insert_all[]=[

“roleid”=> $role_id.

。"node_id"=> $node[0],

“level"=> $node[1],

"pid"=_> $node[2],

];

}

$db access->insertAll($insert_all);

}

Db::commit();

return true:

} catch (\Exception $e){

Db::rollback();

return $e->getMessage();

}

}

代码分析:代码使用的Think PHP的数据库事务处理,方法参数为角色id($role id)和待插入权限数据($data),访问rbac配置文件的access table值读取到access表名,先删除当前角色的所有现有权限,然后通过foreach遍历$data的node id数据,把数据再组装成access表结构数据$insert all,最后通过Db::insertAII把数据一次。降插入到access数据表中,完成角色权限的保存。在代码中利用了事务机制很好的保障了角色权限数据的完整性和有效性。

(2)用户授权

用户表的核心字段有id、account、realname、password等,用户角色表包括字段有role_id,user_id,其中 role_id對应role表的id,user_id对应user表的id,一个用户分配了一个角色就会生成一条用户角色表记录,一个用户可以同时拥有多个角色,同一个角色可以被分配给多个用户。

分配角色的逻辑过程为:首先基于user表创建一条用户记录,然后基于role_user表为该用户创建一条或多条角色分配记录。程序中实现该过程是通过用户管理和角色管理来实现的。

在“角色管理”中选中待分配的角色项,点击对应的“用户列表”功能按钮,程序会弹出当前系统用户列表,然后再勾选需要分配的用户,保存即可。该过程使用到的代码是AdminRole\user方法,源码片段为:

public function user(){

$roleid=$this->request->param(‘id/d);

if ($this->request->isPost()){

if(! $role_id){return ajax_return_adv_error(“缺少必要参数”);}

$db role user=Db::name(“AdminRoleUser”);

$db role user->where(“role id”,$roleid)->delete();

$data=$this->request->post();

if (isset($data[‘user_id])&&!empty($data[‘user_id])&&is_array($data[‘user_ id])){

$insert_all=[]

foreach ($data[‘user_ id]as$v){

$insert_all[]=[

“role_id”=>$role_id.

“user_ id”=>intval($v),

];

}

$db_role_user->insertAll($insert_all);

}

return ajax_return_adv(“分配角色成功”,”);

) else{

if (!$roleid){

throw new Exception(“缺少必要参数”);

$list user=Db::name(“AdminUser”)一>field(‘id,account.realname)->where(‘status=l AND id>1)->select();

$list role user=Db::name(“AdminRoleUser”)一>where(“role_id', $roleid)->select();

$checks= filter_value($list__role_user.”user_id,,true);

$this->view->assign(‘list,$list user);

$this->view->assign(‘checks,$checks);

return $this->view->fetch();

}

}

代码分析:当用户以Get方式提交时,程序执行else部分,Get参数为role_id,程序依次从数据库读取当前系统非超管用户(id>l,数据库设计超管账号id=l)信息到$list_user;读取当前角色已授权信息到$list_role_user,最后把获取到的信息以视图变量的方式指派到对应视图模板。

当用户以Post方式提交时,表示用户是需要保存用户角色信息,程序执行$this->request->isPost()为真的部分,Post参数形如,array(2){[“id”]_>”3”,[“user_id”]=>array(1){[0]=>”2”}},意为把id=3的角色分配给user_id=2的用户。首先,程序为先删除当前角色之前的所有用户,然后,再把当前Post数据简单处理后一次性新增到role user表中。

(二)RBAC权限的验证

我们可以把ThinkPHP程序中所有控制器分为两类,一类是公开性控制器,该类控制器是公开的不需要授权,例如登录控制器、对外接口性控制器等。另一类是授权控制器,该类控制器必须登录授权后才能访问,例如系统权限管理、用户管理、分组管理等。

系统中怎样实现授权控制器的访问拦截的呢?系统在模块根目录下设计创建一个通用基类Controller(app\xfadmin\Controller.php),然后在创建授权控制器时,先让其继承该基类,这样就可以把验证过程放在基类的构造函数中,只要用户访问授权控制器,就会先执行验证程序,从而实现RBAC控制器的实时访问授权验证。

下面将具体通过拦截基类Controller部分源码片段,来简单分析该验证过程。

1.用户UID及ADMrN常量的设置

当用户正常登录后,系统会在Session中创建一个用户UID标识,用于记录用户的系统ID号,后续会通过该UID来获取用户获得的权限树,同时还会创建一个超级管理员ADMIN标识,用于记录当前用户是否是超管账号。这两个常量是在通用基类Controller中定义的,因此在所有授权控制器的业务处理过程中都是可以随时访问到的。源码片段为:

public function____construct(){

defined(‘UID)or define(‘UID,Session::get(Config::get(‘rbac.user_auth_ key)));

defined(‘ADMIN)or define(‘ADMrN,true===Session::get(Config::get(‘rbac.admin_auth_key)));

代码分析:通过分析以上代码片段,可知当用户登录后系统会把登录用户的UID标识和ADMrN标识记录到系统Session中,每当授权控制器被访问时,就会实时读取出来作为验证用户权限的标识常量。其中,rbac是权限管理专用配置文件,在模块文件夹中的extra 中,ThinkPHP引導时会自动引入,在该配置文件中可以动态配置Session中UID和ADMrN的缓存关键字。

2. 用户登录验证和访问权限验证

当用户UID不存在,即表示用户未登录,系统会引导用户跳转到登录页面完成登录,从而保障了系统资源不被非法访问。源码片段为:

public function____construct(){

……

if (null===UID){

$this->notLogin();

} else{

$this->auth();

}

}

当用户正常登录,则需要对用户当前访问权限进行验证。以下是auth方法源码:

protected function auth(){

if (

Config::get(‘rbac.user_ auth on)&&

!in_array($this->request->module(), explode(‘,,Config::get(‘rbac.not_auth_module)))

){

if (!\Rbac::AccessCheck()){

throW new HttpException(403,“没有权限”);

}

}

}

代码分析:当rbac配置文件中“user auth on”项打开,并且当前模块不属于非授权模块,则需要进行权限验证,验证函数为\extend\Rbac::AccessCheck方法,该函数需要传人待验证的权限节点(模块\控制器\方法)。然后,根据用户UID到系统中取出当前用户的权限列表accessList;最后使用keyExist函数判断当前node是否存在于accessList 中,存在则返回true表示当前node被直接授权可以访问,否则返回false表示不可以访问。可见,通过以上授权验证机制,使得程序有效地保障了系统用户不会越权使用权限范围以外的系统资源。

结束语

访问控制技术作为国际化标准组织定义的五项标准安全服务之一,是实现信息系统安全的一项重要机制。基于角色的访问控制( RBAC)是目前流行的先进的安全管理控制方法,解决了传统的访问控制方式DAC、MAC 中的一些问题。本文基于RBAC基本模型RBACO,以ThinkPHP和MySQL为开发环境,实现了一种适用于小小企业的权限管理系统,能针对系统所有控制器方法进行权限的设置和分配,实现了一种约束型权限分配机制,当用户在进行权限分配时,仅能分配自己当前所拥有的权限;另外,还实现了一种公共授权机制,简化了用户授权的操作复杂度。目前,程序仅实现了RBACO基本模型功能,后续将需要进一步完善角色层次设计。

参考文献:

[1]马林.J2EE企业级应用系统及其安全框架研究与实现[D].西南交通大学,2007.

[2]刘建圻,曾碧,郑秀璋.基于RBAC权限管理模型的改进与应用[J].计算机应用,2008,28 (9):2449-2451.

[3]陈波,于冷,肖军模.计算机系统安全与技术[M].北京:机械工业出版社,2009.

[4]李兰崇.基于角色的权限管理访问控制系统平台研究与实践[D].兰州大学,2009.