基于JSON的权限树与角色树设计
最近在考虑设计比较完整可扩展的权限控制机制,希望能够通过这种机制来集中进行权限控制,并借助AOP将权限控制与功能模块分离。
原始的做法来源于操作系统的ACL表,建立一张角色与权限的二维表格,角色与权限相交处的值就表示该角色是否具有该权限(或者用数值、集合代表拥有该权限的“程度”),使用查表的方式已经可以达成AOP控制的前提条件。不过,缺陷在于ACL表的表达能力有限,角色和权限都不具有层次性(无法利用继承性简化权限设定操作的复杂度)。
另一个问题是,如果一个用户可以拥有多个角色,那么当多个角色之间的权限设定值不同时应该如何取舍也是一个难题。
首先为了改善角色的层次性,将角色建立成树形的数据结构,即在数据库中新增一个parent字段(双亲表示法)指出其父角色,需要注意的是,父角色的权限较低,子角色继承了来自父角色的所有权限,还可以具有自己的权限。因此,处于Root的角色,实际上是整个应用中最低权限的角色(角色的默认权限)。对于这个角色的权限描述,则使用灵活性和语义都很强的JSON数组,JSON对象中的key就是权限的key,value就是权限设定值。
在继承性上,也同样遇到该用户具有的不同角色的权限设定可能不同的问题,也还不清楚子角色的权限继承行为具体是怎样的。为了解决这个问题,我们引入一个权限的positive属性,来标识该权限是一个积极的属性还是一个消极的属性,另外,还需要一个属性type来标识该权限是数值型、布尔型还是集合类型(在这里讨论这三种类型的权限)。
- 布尔型非常容易理解,就是“是”或“否”、“能”或“不能”,如果positive,就可以描述是否具备某种权限,否则可以描述是否具有某种限制。
- 数值型用来描述某种程度,例如“自我介绍的最大长度”、“最短发帖时间间隔”等,如果positive,则表示数值越大权限越高,否则数值越小权限越高。
- 集合类型表示一个特定的类别序列,例如“可以上传的文件类型”等,如果positive,则集合表示允许的类型序列,集合越大,允许操作的类别多,权限高,否则集合表示禁止的类型序列,集合越大,允许操作的类别越少,权限低。
基于这三种情况,我们首先考虑权限继承的问题,在考察某个角色的权限时,首先从该角色开始在JSON对象中搜索该操作所需的权限,无论找到与否都必须继续查找父角色中对该权限的设置,直至查找到根,这时我们自底向上取得了一系列对于该权限的设置,然后我们需要进行合成,合成根据上面权限的三种类型以及positive的值。
- 布尔型:positive表示对所有这些权限值进行或运算,即一旦出现true则结果为true;否则对这些权限值进行与运算,即一旦出现false则结果为false。
- 数值型:positive表示取所有权限值的最大值;否则取所有权限值的最小值。
- 集合类型:positive表示对所有权限值进行并运算;否则对所有权限值进行交运算。
如果一个用于具有多种角色,那么在计算得到每个角色的实际权限后,再根据上述规则合成这些角色的权限值。
根据上面的思路,角色已经成为树结构,但是权限还是普通表结构,因此,我们考虑允许权限的分组,将权限同样构造为具有层次的树结构。为了使树有意义,我们需要限定非布尔类型的权限只能出现在权限树的叶子节点上。这样,对于非叶子节点,既表示一种权限设置,又决定了其子权限设定是否有意义。
为了使得权限设置更加简单,我们甚至还可以引入三状态变量,表示Full、None或者Partial,加快了权限查找效率,有时也简化了某些权限的设定。
审慎而明智地使用SQL触发器
触发器可以说是数据库中的常用工具,不过,这个功能是否被合理利用,也在一定程度上影响了最终成品的各方面性能。
要想把说明触发器的适用范围,就得先说说触发器本身具有哪些特点:
- 各种DBMS实现不同的触发器语法,方言之间的差异很大,表达能力上也不尽相同。
- 触发器是对于一张表的监视,从属于某一张表(一个关系),但是被触发时可以作用于其他表。
- 从逻辑上,触发器对于程序员应当是透明的。
基于触发器的上述特点,并站在一个软件全局的角度来看,触发器应该保持对于开发人员的透明性,也就是说,开发人员应当始终不需要知道某个触发器正在对业务逻辑产生影响。
最好的做法就是,使用触发器时,避免使其影响业务逻辑本身,曾经在教科书上看到这样一个例子:
有一个学生选课情况表,字段有学号、课程号、平时成绩、考试成绩、总评成绩。需要设计一个触发器,当学生的平时成绩或考试成绩修改时,就自动按照平时成绩20%和考试成绩80%的比例计算出总评成绩。
实际上这应该是个反面教材,这个使用情形就是刚才所说的触发器干涉了业务逻辑。那么为什么不要让触发器干涉业务逻辑呢?
开发人员站在整个数据库的外侧,也就是关注到数据库的外模式,而不关心或无权过问概念模式和内模式,触发器属于概念模式范畴(甚至可能是更低的层次),不需要被开发人员所了解。那么,触发器对于业务逻辑的干涉将使开发人员对于结果产生困惑,为什么开发人员在看似不完整的数据操作之后(修改了平时成绩或考试成绩后没有修改总评成绩),却得到了完整的结果。这个说法建立在表“充当”外模式使用的前提下。
另一方面,触发器带来了程序员无法控制和了解的业务逻辑,使得软件测试最终变得困难重重,如果有一个BUG出现在触发器中,那么它可能将最后才能被发现,此时已经带来了高额的测试和调试成本。对于数据库管理员也是如此,由于存储过程和触发器处于DBMS三层模型中的不同位置,所以当检查外模式的存储过程的业务逻辑时,也同样会产生不必要的困惑。更严重的情况是,一旦存储过程与相关的触发器单方面进行了改动,最坏的情况可以导致业务崩溃,因为不同层级之间的高耦合带来难以预料的维护上的灾难。
对于上面这个简单的问题,其实使用视图就可以很容易地解决,总评成绩=平时成绩×20%+考试成绩×80%,这样的简单关系通过视图,也不会显著产生效率问题,因为计算不涉及多表。对于涉及许多表而可能影响效率的情况,可以采用存储过程来包装对于平时成绩或者考试成绩的变更,当然对于支持物化视图的DBMS,也可能是不错的选择。
教材上还有一个例子,也针对上面那张表:
设计一个触发器,当删除一条学生的选课记录时,自动在另一张与此表结构一致的备份表中插入这条被删除的记录。
这个例子可以被认为是正确地使用了触发器,它与业务逻辑无关了,即使这个触发器不存在,业务也可以照常进行下去,因为备份表对于开发人员而言实际上也是透明的,只是基于数据可靠性和系统性能考虑而增加的关系,与触发器搭配使用再恰当不过。
另一个使用触发器的场合是统计信息,例如记录用户登录失败的操作、记录用户的操作习惯等等。
但是记得别再一张表上建立太多的触发器哦,否则这张被无数触发器监视起来的表,一旦需要执行DML操作,性能就严重受到影响了。