Teddy's Knowledge Base

一种高性能Hierarchical RBAC实现方案

背景

 

框图

h_rbac.GIF

上图中,Role和被设置PermissionResource都是可以有任意层级继承关系的。

 

举例

 

举一个网站的例子来说:

 

如果,User表示网站用户;Role表示角色;Resource表示所有可访问的URLPermission是对每一个URL的某一个权限(如:查看,修改等)。

 

Role可以有任意层级继承关系,如:用户角色可以分为Normal UserAdmin UserAdmin User下又可以分为Super AdminContent AdminSupport Admin等。

 

URL这种Resource也可以有任意层级继承关系的,如:对http://abc.com/A/A1/A11/A111.aspx这样一个链接,可以认为http://abc.com/A1是一个URL Resourcehttp://abc.com/A/A1/A11是他的一个子Resourcehttp://abc.com/A/A1/A11/A111.aspx又是http://abc.com/A/A1/A11的一个子Resource

 

对某一个Role来说,他对某一个Resource – R1的具体的Permissions,等于关联到这个RoleResource - R1及其所有父级ResourcePermissions的并集。

 

对于某一个User来说,他对某一个ResourcePermissions,等于他所属的所有RolesPermissions的并集。

 

问题

 

各元素之间的关系容易理解,关键的难点在于,因为RoleResource都可以是有无限层级继承关系的,如何保证权限信息验证具有较高的性能呢?当继承关系较复杂时,递归检测的性能无疑是不可接受的。

 

数据库表

 

UserIDName

RoleIDNameParentIDLeftIndexRightIndex

UsersInRolesUserIDRoleID

ResourceIDNameParentIDLeftIndexRightIndex

PermissionsOfRolePermissionsValueResourceIDRoleID

 

这里简单起见,对于Permissions,使用一个二进制位表示一个具体的Permission。我们需要事先定义一个PermissionsValue的每一个二进制位表示的Permission。例如:如果PermissionsValue的二进制值为10101010,表示从低位到高位第2468位所代表的权限的并集。

 

使用二进制位表示一个具体的Permission的好处是,处理Permissions的并操作可以转换为二进制的OR;缺点是,具体的Permission想不能特别多,因为多一个就意味着PermissionsValue的最大值大一个2的次方。8位二进制的最大值是28次方,这不算很大,但是,1000位二进制的最大值是21000次方,这就是个不可想象的巨大数字了。好在,一般来讲,具体的Permission项目不会特别多的。

 

该方案的关键,就在于RoleResource表的LeftIndexRightIndex这两个字段了,我们将使用这两个字段,在避免递归的情况下,实现较高性能的取某个继承节点的所有子元素或所有父元素的算法。

 

算法

 

我们以Role为例,首先Role表中有且只有一条记录存放所有Roles的顶层父节点(1,“Root Role”,12)。当他没有子节点时,其LeftIndexRightIndex的值分别为12。当对其插入子节点时,LeftIndexRightIndex的值需要做相应的调整,调整的规则如下(括号中为LeftIndexRightIndex的值):

h_rbac_alg.GIF

按逆时针方向,大家能看出规则吗?

按照这个规则,我们可以如下获取某一个节点的所有字节点或所有父结点(使用伪SQL代码表示):

 

获取ID3Role节点的所有的子结点包括本身:

SELECT * FROM Role WHERE

LeftIndex >= (SELECT LeftIndex FROM Role WHERE ID = 3)

AND

RightIndex <= (SELECT RightIndex FROM Role WHERE ID = 3)

注:如果要不包括ID=3的节点本身,只需要用><代替>=<=

 

获取ID5Role节点的所有父节点包括本身:

SELECT * FROM Role WHERE

LeftIndex <= (SELECT LeftIndex FROM Role WHERE ID = 3)

AND

RightIndex >= (SELECT RightIndex FROM Role WHERE ID = 3)

注:如果要不包括ID=5的节点本身,只需要用><代替>=<=

 

大家可以根据上面的图验证一下算法的效果。完全不需要递归,只需要简单的判断LeftIndexRightIndex就行,性能自然是非常好的。

 

我们甚至可以以非常简单的SQL语句获得某一个ID2UserID6ResourcePermissionsValue

 

DECLARE @PermissionsValue int;

SELECT @PermissionsValue = @PermissionsValue | PermissionsValue

FROM PermissionsOfRole WHERE

RoleID IN

(

SELECT ID FROM Role WHERE

LeftIndex >= (SELECT LeftIndex FROM Role WHERE ID IN

(SELECT RoleID FROM UsersInRoles WHERE UserID = 2))

AND

RightIndex <= (SELECT RightIndex FROM Role WHERE ID IN

(SELECT RoleID FROM UsersInRoles WHERE UserID = 2))

)

AND

ResourceID IN

(

SELECT ID FROM Resource WHERE

LeftIndex <= (SELECT LeftIndex FROM Resource WHERE ID = 6)

AND

RightIndex >= (SELECT RightIndex FROM Resource WHERE ID = 6)

);

SELECT @PermissionsValue;

上面的SQL虽然有不少嵌套的SELECT,但是,因为子查询基本上都是对主键字段的条件判断,LeftIndexRightIndex我们也会加上索引,因此,实际上不会对性能造成太大影响。

 

OK,查询性能很好,不过这是以新建或修改RoleResource的层级关系时的一定的性能损失为代价的。每次新增或修改RoleResource的层级关系时,必须按照前面所述的规则重置所有节点的LeftIndexRightIndex值。不过,一般情况下,由于RoleResource的维护操作占系统整体操作的比例很小,几乎可以忽略,因此其性能损失也不是什么大问题。具体的重置所有节点LeftIndexRightIndex值的伪代码我就不贴出来了,大家稍微花费几个脑细胞就能想出来了^-^

//结束

Tag标签: rbac

posted on 2008-01-23 21:29 Teddy's Knowledge Base 阅读(3261) 评论(24)  编辑 收藏 所属分类: Ent. App. Dev.Tech. Thinking

评论

#1楼  2008-01-23 22:01 Hunts.C      

关注   回复  引用  查看    

#2楼  2008-01-23 23:04 zoti      

做个记号   回复  引用  查看    

#3楼  2008-01-23 23:25 代码乱了      

好,明天好好看一下   回复  引用  查看    

#4楼  2008-01-23 23:49 Klesh Wong      

图错了吧?
Root Role的RightIndex应该是12吧?

呵,这样一来就没办法移动节点了吧。   回复  引用  查看    

#5楼  2008-01-23 23:51 saucer [未注册用户]

在想,在一个网站项目里,大概不会每次用户访问,就去数据库查询权限吧(当然,内部项目,访问人数少是例外),那么一般的做法也许会是以某种缓存的形式缓存用户权限(大概是以用户权限对象实例的形式),那么这样的复杂性也许可以通过代码实现

Mr.Oren Eini 好像也在实现类似的东西
http://www.ayende.com/Blog/archive/2008/01/22/Rhino-Security-Overview-Part-I.aspx   回复  引用    

#6楼  2008-01-23 23:54 kiler      

LeftIndex,RightIndex如果是纯粹提高性能的话,不要也可以,把所有数据读到DataSet里在内存里递归也可以,性能损失也不大,毕竟Role表和Resource表里面的数据量是相当的小的。   回复  引用  查看    

#7楼  2008-01-24 02:34 Hightree      

思路很好,很有启发,增加插入、修改的复杂度,降低查询时间。
不过真正的企业权限,好像还不止这些,例如:对某个对象,操作可能不只是yes和no的,包括查看、复制、增加、修改、标记过期、标记删除、授权,甚至包括用户自定义权限。另外权限除了按照role定义外,还可能有各种五花八门的要求,例如某个组的人都有权限但某几个人除外,对于某条记录,在某段时间内临时授予几个人权限,等等。   回复  引用  查看    

#8楼  2008-01-24 08:26 王立斌      

我看这样的东西还是很吃力的。不过相信你跟我的水平已经不在一个高度了。   回复  引用  查看    

#9楼 [楼主] 2008-01-24 09:22 Teddy's Knowledge Base      

@Klesh Wong
图没错,新增,移动,删除任意节点后都需要重设所有节点的LeftIndex和RightIndex的。

@saucer
考虑有100000个用户,其中100个管理员可以修改节点。每个用户登录时至少都要读取计算一次他的权限,所以读取的频率是比较大的,而修改节点的比例很小,该方案对性能的好处才会有体现,对小项目和节点关系简单的项目来说,用什么方案都无所谓的。

@kiler
你说的方案一定程度上也可以,但如果权限很多很复杂,web服务器或app服务器有没必要的额外开销,Select in DataSet的开销挺大的。

@Hightree
基于该方案是很容易进行扩展支持你说的复杂情况的,我实现的是相对比较标准的Hierarchical RBAC标准。   回复  引用  查看    

#10楼  2008-01-24 10:00 kiler      

@Teddy's Knowledge Base

能在详细的解释一下PermissionsValue的意思吗,本人水平有限,没看明白,多谢了 :)

  回复  引用  查看    

#11楼  2008-01-24 10:12 cozo [未注册用户]

对于百万级用户,每次访问的时候执行一次这个SQL语句,性能好不了。   回复  引用    

#12楼  2008-01-24 10:36 踏浪      

数据范围的权限要如何处理?   回复  引用  查看    

#13楼 [楼主] 2008-01-24 11:14 Teddy's Knowledge Base      

@cozo
实际的系统中当然会做缓存,不可能每次直接用这样的语句来查询。我这里给出语句的目的是为了和递归的情况比较。

实际上,本方案中特色的部分在于使用LeftIndex和RightIndex字段避免层级递归,给出一种性能较高的思路,并不是要提出一种万能的RBAC方案,实际的系统中肯定要根据实际情况进行扩展或简化的。这样的思路对任何包含多层级的数据的父子查询都是类似的,也不仅仅局限于权限控制中。   回复  引用  查看    

#14楼  2008-01-24 11:17 双鱼座      

如果真的有100000个用户,添加、移动或者删除一个role(特别是level高的role)那可麻烦大了。
其实你讲的这个主题与rbac没有关系,其实只是个hierarchical的问题。既然要加两个字段还不如另加一张表,这张表仅仅记录所有的祖先类。
你的思路是尽可能使用update更新数据库,我的思路是尽可能使用insert/delete以避免伤及无辜的记录。
关于hierarchical我有篇文章写了五年,还没有脱稿。   回复  引用  查看    

#15楼 [楼主] 2008-01-24 11:34 Teddy's Knowledge Base      

@双鱼座
有多少个用户和添加、移动或者删除Role没有任何关系,权限是基于Role的。添加、移动或者删除Role只会导致更新所有Role的LeftIndex和RightIndex,除非你有几万个Role。使用我这个方案,基本不需要你说的“另加一张表,这张表仅仅记录所有的祖先类”。对性能不会造成太大的影响。   回复  引用  查看    

#16楼  2008-01-24 11:53 ravezhang [未注册用户]

我想问一下,按照上面给的例子,是不是只能应对一个用户被赋予唯一一个角色的情况呢?如果一个用户可以被赋予多个角色的话,那么给出的查找某个用户对某种Resource的PermissionsValue的SQL语句,在处理上面好像会有问题啊!   回复  引用    

#17楼 [楼主] 2008-01-24 12:23 Teddy's Knowledge Base      

@ravezhang
当然不是,第一张图就说明了User和Role是多对多的。   回复  引用  查看    

#18楼  2008-01-24 13:09 双鱼座      

@Teddy's Knowledge Base
你可能误解我的意思了,其实性能问题无非是是否需要递归。同样不需要递归,对于查询的性能来说,加两个字段与加一张表没有什么差别。但是对于添加、移动和删除的性能来说,差别可就大了去了。
“你的思路是尽可能使用update更新数据库,我的思路是尽可能使用insert/delete以避免伤及无辜的记录。 ”   回复  引用  查看    

#19楼 [楼主] 2008-01-24 14:01 Teddy's Knowledge Base      

@双鱼座
同意你的思路确实尽可能“避免伤及无辜的记录”。对于权限管理中的Role或Resource,他们的Hierarchy不会有特别多记录,修改的性能差别我觉得不是太大的问题。从数据库维护的角度,个人觉得多一张表的味道还是比多两个字段似乎要难受一些。而且你的表里包含了很多的冗余信息,味道也不是特别好,我虽然多了两个字段,但是,从数据的概念上说,并不冗余,而且,根据我的这个思路,除了能实现方便的取得所有父节点或所有子节点外,还能有一些额外的作用,因为我是一棵有特殊规则权值树,使用较灵活,利用这些权值进行取父子节点的查询只是它的用途之一。   回复  引用  查看    

#20楼  2008-01-24 15:20 Wendy Yu      

Root Role的RightIndex应该是12才对吧。

Root Role的所有子节点:
SELECT * FROM Role WHERE

LeftIndex >= 1
AND

RightIndex <= 12
  回复  引用  查看    

#21楼 [楼主] 2008-01-24 15:33 Teddy's Knowledge Base      

@Wendy Yu
的确应该是12,谢谢发现这个问题,已经改正了。   回复  引用  查看    

#22楼  2008-01-24 19:06 双鱼座      

@Teddy's Knowledge Base
看来说服你放弃你的方案有点困难。加一张表来记录所有的祖先节点ID肯定是冗余,但是你加两个字段也是冗余。我们可以假定所有可以推导出来的数据都是冗余的,你所加的字段都是可以推导出来的,所以仍然是冗余。
其实设计数据库表或者字段是否“要难受一些”,不是靠感官,而是逻辑。通常基数不等的元素应该设计成分开的实体,也就是多表,节点和它的祖先集合、子孙集合显然基数是不同,幻想在一张表里解决一对多一定会带来某种不便,我说的添加、移动、删除对你的方案就已经出现了不便。如果有100个角色节点,刚好我动了根节点的第一个子节点,我要锁定其余的99行记录。虽然动的可能性很小,但不是没有。同时锁定这么多记录的后果就不用我说了。
也许你还有其他的用途,虽然我不知道你的用途是什么,但我可以确定的是:要实现你的这些用途,加表的方案一定比加字段的方案更灵活,副作用更小。   回复  引用  查看    

#23楼  2008-01-25 12:12 fuck huawei [未注册用户]

思路不错   回复  引用    

#24楼  2008-08-03 08:47 蛙蛙池塘      

TEDDY最近去哪儿了?   回复  引用  查看    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-01-23 21:41 编辑过


相关链接: