权限部分将分两章介绍,第一章由浅入深介绍权限理论知识及应用,第二章介绍具体实现。后期再讲述中间件的使用时,还会插入一些权限内容,本质上属于中间件的应用。
权限模块是业务系统最常见、最基本的子集。本章假定了一个系统从最初简单的需求到逐渐成熟且完善的权限体系的实现过程。
阅读本章预计花费20分钟。
1. 最简单的权限模型
业务系统初期,需求简单,对于权限的内容本身并不复杂,我们假设权限部分仅有这样简单的需求:
能给用户赋予数据的增、删、改、查四种权限
分析此需求,权限的主体为用户,权限的内容有多种,关系为M - M,具体为:
用户模型:
public class User
{
public int UserId{ get; set; }
public string UserName { get; set; }
}
用户表Auth_User
字段 | 类型 | 说明 |
---|---|---|
*UserId | int | 用户ID |
UserName | varchar | 用户名 |
... | ... | ... |
权限枚举:
[Flags]
public enum Permission
{
Add = 1,
Update = 2,
Delete = 4,
Select = 8
}
权限表 Auth_Permission
字段 | 类型 | 说明 |
---|---|---|
*PermissionId | int | 权限ID |
Permission | varchar | 权限内容 |
用户-权限关系表 Auth_UserPermission
字段 | 类型 | 说明 |
---|---|---|
*UserId | int | 用户ID |
*PermissionId | int | 权限ID |
假如一个用户有增、改两种权限,那么关系表(Auth_UserPermission)可以存储为:
UserId | Permission |
---|---|
1 | 1 |
1 | 2 |
于是对于权限的基本操作我们可以进行归纳:
- 授权:INSERT 权限表 (用户ID,权限的具体值)
- 校权:EXISTS 权限表 UserID==用户ID AND Permission==要判断权限的具体值
我们留意到对于Permission的枚举定义,值使用了对2的幂运算的值:
幂运算 | 十进制 | 二进制 | 十六进制 |
---|---|---|---|
2^0 | 1 | 0001 | 0x01 |
2^1 | 2 | 0010 | 0x02 |
2^2 | 4 | 0100 | 0x04 |
2^3 | 8 | 1000 | 0x08 |
这么定义是有好处的,对于Auth_UserPermission的表存储可以节省存储空间,并且程序便于处理,譬如:
如果UserId=1的用户拥有Add、Select权限,Auth_UserPermission表原本应该存储两条记录:
- (1,1)
- (1,8)
现在,可以考虑更简单的存储方式
- (1,9)
这表示:
Permission.Add | Permission.Select
等价于
1 按位或 8 ( 1 | 8 )
等价于
9
而对于权限的判断,则使用存储的权限值按位与要进行校权的值是否等于要进行校权的值来判断
譬如判断用户是否拥有Delete权限,则使用9按位与4是否等于4来进行判断,用C#的三目运算来表示为:
9 & 4 == 4 ? "有权限":"无权限"
这样被标记有Flags特性的枚举在.Net框架中遍布各种基础类库,譬如反射中的BindFlags枚举。本身属于基础知识,由于不常应用所以容易被忽视,在权限中属于应用小技巧。还有人质疑这么存储会有性能问题,在后面章节讲到优化时,再行讨论。
于是我们对使用了小技巧的新的权限基本控制再次进行归纳:
- 授权:INSERT 权限表(用户ID,所有拥有权限的按位或值)
- 校权:EXISTS 权限表(UserID == 用户ID AND Permission & 要判断权限的具体值 == 要判断权限的具体值)
2. 基于角色的基本权限控制
随着业务系统的发展,业务系统有了第一次升级机会,并附带了一个新的权限需求:
系统需要满足一类职位的人拥有相同的权限
按照第一节的内容,这个需求其实不用做任何变化一样可以满足,但是问题在于负责授权的人“太累了”,对于每一个用户,我们可能都要做一遍授权的操作。
为了解决这个问题,我们引入角色这一基本单元,角色是一种抽象,可以具体到业务场景的类似职位、身份等概念。
角色模型设计:
public class Role
{
public int RoleId { get;set; }
public string RoleName { get;set; }
}
角色表设计Auth_Role:
字段 | 类型 | 说明 |
---|---|---|
*RoleId | int | 角色ID |
RoleName | varchar | 角色名称 |
基于角色的基本权限控制的原则是:
- 简化用户权限的操作;
- 权限操作的对象从用户变更为角色;
- 不能对单一用户做权限操作,仅对角色做权限操作,每个需要权限的用户,都拥有至少一个角色;
角色与用户的抽象关系表现为M-M,这表示:
- 一个用户可以拥有多个角色;
- 一个角色下有多个用户;
具体到业务可以是一个人可以有多个职位;一个职位下有多个人;
针对此设计,我们需要做以下操作:
- 从系统中删除掉原来的Auth_UserPermission关系;
- 新增Auth_UserRole(UserId,RoleId)的关系;
- 新增Auth_RolePermission(RoleId,Permission)的关系;
假定业务系统有这样的职位列表:
RoleId | RoleName |
---|---|
1 | 总裁 |
2 | 开发总监 |
假设用户ID等于1001的用户职位为总裁兼开发总监,那么关系表Auth_UserRole可以存储为:
UserId | RoleId |
---|---|
1001 | 1 |
1001 | 2 |
业务约定:总裁有增、删、改、查四个权限,开发总监则有增、查两个权限,那么关系表Auth_RolePermission可以存储为:
RoleId | Permission |
---|---|
1 | 15( = 1 | 2 | 4 | 8 ) |
2 | 9 |
我们对给予角色的基本权限控制操作再次归纳为:
- 授权:给角色添加权限(INSERT Auth_RolePermission),给用户添加角色(INSERT Auth_UserRole)
- 校权:应当是拿出用户所有的角色,并再次拿出这些角色的权限做并集,并DISTINCT 权限并集为权限集合,判断权限集合是否含有需要校权的权限
3. 基于角色并含有用户组概念的权限控制
春去秋来,业务系统迎来了第二次升级机会,并包含以下新的权限需求:
所有部门的开发岗位拥有相同的增、查权限
基于第二节的系统升级,解决此需求我们会有临时的做法:做一个角色,给所有开发岗的同事赋予这个角色。
这样的临时做法的确解决了我们的问题,但这里有几个问题,函待解决:
- 系统没有部门的对应抽象;
- 一旦其中一个部门的开发岗同事拥有的权限有变动,我们需要新建角色,并重新授权;
针对此两个问题,我们引入一个新的模型:用户组(UserGroup),用户组的概念在业务系统中,可以具体为:部门、小组、团队等
用户组模型设计:
public class UserGroup
{
public int UserGroupId { get; set; }
public int ParentId { get;set; } //留意此字段,将在本节末尾阐述
public string UserGroupName { get; set; }
}
用户组表Auth_UserGroup设计:
字段 | 类型 | 说明 |
---|---|---|
*UserGroupId | int | 部门ID |
ParentId | int | 上级部门ID |
UserGroupName | varchar | 部门名称 |
基于角色并含有用户组概念的权限控制有以下特点:
- 再次简化了用户权限的操作;
- 用户可以拥有角色;用户组也可以拥有角色;
- 权限的操作对象依旧为角色,不可对用户、用户组进行权限操作;
用户与用户组的关系表现为多对多,这表示一个用户可以属于多个用户组,一个用户组下可以有多个用户,具体到业务可以描述为:一个人可以在多个部门,一个部门下可以有多个人;
用户组与角色的关系表现为多对多,这表示一个用户组的所有用户可以拥有相同的多个角色,一个角色下有多个用户组,具体到业务可以描述为:同一个部门的人可以拥有多个相同的职位;
为了实现此设计,我们需要做以下新的操作:
- 新增Auth_UserUserGroup关系;
- 新增Auth_UserGroupRole关系;
假设系统拥有这样的部门列表:
UserGroupId | UserGroupName |
---|---|
1 | 总裁办 |
2 | 前端开发部 |
3 | 中台开发部 |
4 | 人力资源部 |
5 | 保安部 |
假设用户ID为1101的用户既是前端开发部的开发总监,又是中台开发部的开发总监;中台开发部、前端开发部的所有同事本质都是开发,且所有开发部的同事都有增、查的权限,那么:
用户-用户组Auth_UserUserGroup关系表可以存储为:
UserId | UserGroupId |
---|---|
1101 | 2 |
1101 | 3 |
新增角色:开发
RoleId | RoleName |
---|---|
6 | 开发 |
Auth_RolePermission新增记录:
RoleId | Permission |
---|---|
6 | 9 |
Auth_UserGroupRole关系表可以存储为:
UserGroupId | RoleId |
---|---|
2 | 6 |
3 | 6 |
这样,我们就满足了本节提出的需求。
另外要注意到的是用户组的ParentId字段,不要轻视这个简单的树状设计,实际应用中根据业务场景会有各种不同的问题,譬如不良的SQL导致DB层面做了递归查询、上级部门权限与下级部门权限的继承关系,但这本质属于业务需求,不再赘述
4. RBAC权限模型
现在,系统经过3次升级,已经有了较为完备的权限体系,我们解决了大部分问题。
但是我们也注意到,所有的有关于权限的定义仅仅围绕着增删改查这一类权限控制。假如系统现在需要多控制一部分权限内容,我们就有些捉襟见肘了。
简单来说,我们的权限模型设计对于扩展支持不够
譬如,业务系统初期对系统的菜单可见性有权限控制,随着系统迭代,可能出现对文件的可操作性也需要有权限控制,这是很正常的事,显然,依照我们的设计,系统无法满足。
回顾1、2、3节的升级内容,我们的问题其实是由单一权限元素变更为多元权限元素,如果我们能重新将被控制元素变更为单一元素,我们之前的设计则不用变更。
为了解决这个问题,我们对各种权限元素进行抽象,譬如文件访问权限和菜单访问权限。抽象为如下图内容:
现在,权限的Root节点变成了Permission这个抽象,它没有具体的意义,但他将各类权限集中在了一起,使得多种权限元素重新集中在单一Permission这个抽象元素上,再次揉入到我们的系统中,如下图:
这就是权限系统的RBAC完成模型。
至此,借助RBAC模型,我们完成了权限模块的理论设计,它能满足大量权限控制场景,也是业界惯用的手段,RBAC模型是一种权限模型的总结和归纳,市面上能见到的各种权限控制,都与RBAC沾边,也就是说,掌握RBAC,就掌握了阅读各种系统权限设计的基础,有了理论支持。
不过值得注意的是,虽然我们有了理论基础,但实际应用中,我们还要做一些扩展内容。
譬如说权限历史,权限模块属于敏感内容,是系统的中枢所在,严谨的权限模块肯定是不会对操作进行Delete的,而是Fake Delete以保留历史。上文中这样的设计为此提供了方便,当用户的权限发生变更时,我们只需要对关系做Fake Delete即可。当然,关系本身需具备IsFakeDeleted属性。
下一章节将介绍dotnet core的具体实现。