软件项目最佳实践: 又谈权限管理

一个架构的好坏,判断标准是:是不是容易维护.
代码量少,这是最重要的一个指标.

权限管理也很重要,也是大家最感兴趣的一点,今天工作中又聊到这个话题,又有新意,将来再讨论。

 

统一的权限架构有什么好处?统一的验证逻辑,统一的配置,方便维护。       

不要迷失我们的终极目标:什么人对哪些数据(在什么时候)能做哪些事。

 

映射到角色/功能模型的话:
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


瞧,多直观的角色,还可以更复杂:周一到周五,轮着当经理都行。

岗位 = 角色 + 部门 + 周几。

一个角色附加一些鉴权参数,就可以具体化此角色,成为一个新岗位,而岗位也抽象为一种角色。

 

事实上,这里最不容易实现的是第2点:如何可视化配置,依据PayBill的属性来筛选数据操作权限。

假设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君”设定为一个组,这样赋角色时,操作可以简便一点。

另外,我们也没有引入角色继承的概念,所有角色都是扁平的,避免权限编程模型过度复杂。

 

高手会说:这......是个度的问题。我说:有一个是最佳实践.

posted @ 2012-07-18 23:33  heguo  阅读(1334)  评论(0编辑  收藏  举报