软件项目最佳实践: 又谈权限管理
代码量少,这是最重要的一个指标.
权限管理也很重要,也是大家最感兴趣的一点,今天工作中又聊到这个话题,又有新意,将来再讨论。
不要迷失我们的终极目标:什么人对哪些数据(在什么时候)能做哪些事。
A. 一个人属于多个角色
B. 一个角色能操作哪些功能项
C. 一个角色能读写哪些数据
只要能实现这样的控制逻辑,都是好用的权限管理架构。
我的做法是:不能操作就引发无此权限异常,这样方便编码,更有效地保证安全。
A和B都好理解,实现起来也容易。
针对C,我的做法是:对每种数据的操作都定义一个角色。
也就是说角色有N多,除了预定义的角色外,还能不停地生成。
如,部门经理这个角色是系统预定义的,但还有”一部经理“,”二部经理“,”三部经理“这样的角色,我们称作“岗位”。
基于这个理论,我们还是套用了角色/功能的权限管理模型。
如何来实现岗位呢?听起来有点像实例化的角色对象,实现起来也简单:引入“鉴权参数”的概念。
“部门经理”是预定义的角色 DeptManageRole
此角色有一个属性叫:DeptCode。值 = “一部“,这就是鉴权参数
一部经理就是 new DeptManageRole(){DeptCode="一部"},也是一个角色,在编写控制逻辑,鉴定操作权限时,
除了判断是什么角色外,还要判断部门参数是不是等于一部。
现假设,有这样的一个业务场景:
一部经理可以在付款申请单未审批的情况下付款。
写代码,大家都会:
点击“付款”按钮时,若请款单的属性=未通过,当前用户有部门经理角色,付款单的部门 = 部门经理.部门参数 就继续付款,否则提示“无此权限”。
我们在Asp.Net中通常把用户名序列化到Cookie中,我推荐把所属角色也一并保存到Cookie中,
所以"一部经理"这个角色应可以序列化和反序列化,以便保存到Cookie。
当然保存到数据库中也没问题,需要读写数据库而已,会有性能损失。
//登录后,我们给当前用户赋于“一部经理“的角色:
var role = new DeptManageRole(){DeptCode=CurrentUser.DeptCode};
CurrentUser.AddRole(role);
//检索数据时,当前用户只检索一部数据
getPayBills()
{
payBillDbSet.where(payBill=>payBill.DeptCode = CurrentUser.AsRole<DeptManageRole>().DeptCode).select();
}
//付款时
Pay(payBill)
{
if CurrentUser.CanNot("未审批付款“) then
throw new exception('付款单未审批,不能付款')
end if
if payBill.Status = "未通过" and payBill.DeptCode = CurrentUser.AsRole<DeptManageRole>().DeptCode then
pay....
else
throw new exception('付款单未审批,不能付款')
end if
}
至此,我们的权限管理已经实现了,但——
客户老板打电话过来说:最近有几笔付款单金额很小,不必我批准,让部门经理可以付款,快教我怎么设置,给他分配权限。
这可怎么办?合理需求啊。
1. 权限要可配置。否则我们要改代码,要编译,要重新发布。
2. 要可视化配置,否则客户老板不知道怎么设置,看不懂XML,更不会打开数据库表去修改字段。
3 . 技术上,我们需要有类似这样的配置项:
付款: DataObject.Status = "未通过" and DataObject.Amount < 100.00 and IsRole("部门经理") and DataObject.DeptCode = <部门经理>.DeptCode
当按钮点击时:
if Can(thePayBill,"付款") then //动态计算此表达式,返回true/false,实参是thePayBill,形参是 DataObject
pay...
else
throw new Exception(“不能付款”)
end if
我们再回到角色的配置上来:
角色可以用Xml或Table来定义。
甚至可以是一个二维表:
一部 二部 三部
部门经理 MA1 MA2 MA3
部门副经理 MB1 MB2 MB3
助理 H1 H2 H3
瞧,多直观的角色,还可以更复杂:周一到周五,轮着当经理都行。
一个角色附加一些鉴权参数,就可以具体化此角色,成为一个新岗位,而岗位也抽象为一种角色。
假设PayBill表没有Amount字段怎么办?没有好办法,兴许硬编码是更好的方式。
当然,为了实现此需求,套用我们的权限功能,在PayBill表上增加一个字段就解决问题了,
可是,表上加字段,类上加属性,似乎有背于我们统一基础权限管理愿望,怎么办?
既然配置上统一不起来,我们退一步,至少在代码级别上可以统一使用这样的权限架构。
实际上,试图用配置完全实现权限控制真是很难实现的:
假设用户没权限点击某按钮时,这个功能按钮是显示Visible呢,还是不显示呢?是可用Enable呢,还是不可用呢?
是异常中止呢?还是返回false呢?还是提示重登录呢?还是显示帮助信息呢?
例如,QQ就会经常提示:只有会员才可点击,请立即付钱,成为钻石会员。
有统一的地方设置当然更好,我觉得硬编码也是个不错的选择。
只要代码风格好,逻辑清楚,为什么不一起来维护优美的诗句呢?
这里还有一个变态的角色:
每次当部门经理出差后,A君就要代理部门副经理的角色,审批一些单据。
怎样才能让A君不必重新登录系统,一旦部门经理点击了“我已出差”后,A君就可以做代审批的操作?
套用我们的权限管理架构,可以实现:
审批时:
代经理.领导已出差 = return 判断领导是否在公司();
if CurrentUser.AsRole(代经理).部门 = DataObject.部门 and CurrentUser.AsRole(代经理).领导已出差 = true then
可以批
else
不可以批
end if
很快,这里又产生了一个现实的问题:
通常我们的角色叠加时,对功能项的操作权限也越大,
比如当前操作人员既是一部业务经理,又兼职二部财务经理时,他应该可以点击更多按钮,看到更多数据。
实际编码中发现,如果具体化的岗位来限制数据检索权限时,where子句 and 条件就会叠加,
最终结果是:既看不到一部数据,也看不到二部数据。
如果要叠加数据操作权限,就要用 or 来连接 where 条件子句,这增加了编码复杂度。
推荐做法是(感谢宁波罗经理的建议):
这种情形,我们简化角色模型,用户在登录时,要么选择以一部经理身份登录,
要么选择以二部财务经理身份登录,不要同时混用这两种岗位。
如此,编程模型就简单多了,再也不必纠结在数据检索权限的控制上了。
再考虑一个复杂的权限表:
这样的权限表复杂吧?如果用鉴权参数来设定角色就轻而易举了.
只要定义一种角色:负责人,把部门+产品线+市场作为鉴权参数即可。
那么可视化的权限设置表该怎样设计?预置的权限配置方式肯定不直观,我们还须为此权限表完整地开发一个设置功能,
管理员直接双击单元格,录入人名就可以完成设置。如果是开发人员来设置这个权限表就打开XML文件,可以手动改写。
看来已经没有什么需求能难倒我们了,能想到的都可以套用此权限管理架构,
但我们不明确复杂的业务将来会演变成什么需求,永远没有办法论证未来会发生的事情。
在这里,我们没有引及组(Group)的概念,组就是把“A君和B君”设定为一个组,这样赋角色时,操作可以简便一点。
另外,我们也没有引入角色继承的概念,所有角色都是扁平的,避免权限编程模型过度复杂。
高手会说:这......是个度的问题。我说:有一个是最佳实践.