如何建设一个权限系统
1.权限系统是什么
权限系统就是系统的安保系统,保障系统的功能谁能使用,谁可以操作哪些数据
2.权限系统的组成
- 功能权限
决定了用户能够使用哪些功能
- 行数据权限
决定了用户能够使用哪些行数据
- 列数据权限
决定列用户能够查操作哪些列的数据
3.功能权限系统的实现方式
- tomcat的内置权限
- shiro
- spring security
- 自定义的权限系统
3.1.功能权限的粒度大小
- 粗粒度,只控制菜单的展示
- 细粒度,控制web系统,所有可操作的按钮的展示,同时后端还应该能够对url进行拦截
3.2.如何实现细粒度的功能权限控制
- 每个功能,使用一个namespace表示,比如user.add,user.update,user.delete
- 用户登录时,获取该用户所有的namespace
- 渲染界面时,根据namespace的范围,来决定是否显示该功能
- 后端拦截的实现和前端基本一致,每个功能namespace绑定后端对应的uri
- 后端session存储该用户所有的uri
- 请求时,filter过滤该uri,如果具备访问权限,通过请求
3.3.RBAC(Role Base Acess Control)模型的介绍
权限模型在授权时,把权限都集中收于给Role,然后将Role授予User,达到提高效率的一个方式,但是本质还是把权限授予给了具体的用户。
3.4.基于RBAC的衍生模型
RBAC衍生模型衍生了用户组的概念
3.5.RBAC等模型的本质
无论模型怎么变化,但是最终的权限落地,都是将权限授予用户。在方案选型时,要关注业务的复杂度与哪种模型的匹配度较为吻合,切勿好高骛远,过度选型。
4.行数据权限
数据权限的实现,目前业界并无通用的解决方案,所以这里面主要是介绍一种模型,大家可以借助模型,去发现系统的数据权限模型
4.1识别权限实体
在做数据权限时,第一步就是要识别出权限实体。下面举几个例子
- crm系统,权限实体是客户
- 订单管理系统,权限实体是订单
- 库存管理系统,权限实体是货品
4.2.识别系统要做到哪种级别权限控制
- 只读、只写、读写
- 是否有部门领导
- 是否需要部门传递关系
比如在上面的图片中,假设员工骆宏属于A部门,那么员工A是否能够看到(B,C,D,E,F,G,H,G,K),如果可以,就代表具备传递性
4.3.数据存储的实现方式
- 将权限数据,权限实体实体数据独立出来存储
- 将权限数据寄存与权限实体中
下面分别举个列子
假设有下面的场景,订单1000001,员工A,员工B,其中存储订单的数据叫做lh_order
方案1: [1000001,A]的数据需要存储在一个数据权限表中,我们叫他lh_auth_data,那么就存储一条[1000001,A]
方案2:直接把数据存储在lh_order即可,也就是jt_order中有一条积累[1000001,A]
这两个方案都可以,但是查询的性能会有却别
4.3.1.方案优缺点对比
方案1由于多了一个表,在查询时,可能会导致性能下降,但是却留了一个很好的扩展点,比如1000001同时支持B管理,只需要在lh_auth_data插入[1000001,B]
方案2查询性能较好,但是却扩展不易,假设需要支持B,那么将处理起来非常棘手
4.4.数据的sql查询模型
假设权限实体是:account、product_group、country
select xxx from xx_table where account in (xxx) and product_group in (xxx) and country in (xxx)
4.5.查询性能的问题
在4.4的查询模型中,我们发现使用的是in查询,我们都知道,mysql的in是有效率问题的,当数据规模来到kw级别时,上面的模型就会出现性能的极速下降。
为了突破该性能限制,我们借助tree,以及mysql的前置like查询
20180408123706965.png
A: 10
B: 10001
C: 10002
D: 10003
E: 10001001
F: 10001002
...
假设我们能够将权限实体,进一步的用tree来组织,然后将权限实体的in转换为tree key like '10001%'的模型,我们可以发现,即利用上了mysql的索引,又解决了in的效率问题。
4.5.1.小心tree的key陷阱
在上面,我们使用tree key来处理,但是我们注意到,tree key是有数量限制的,比如A的直接子节点,key范围是:[10001,99999],假设超过了该tree key时,需要考虑怎么进行数据保护。
5.列数据权限
5.1数据
我们先看一个简单的列子,用户管理,api返回的数据如下
[
{
"name": "骆宏",
"age": 27,
"tel": "15013336**4",
"school": "广东海洋大学",
"job": "高级开发工程师"
},
{
"name": "骆宏",
"age": 27,
"tel": "15013336**4",
"school": "广东海洋大学",
"job": "高级开发工程师"
}
]
5.2.前端界面的效果
对A用户,无权限控制的现实效果
名字 | 年龄 | 电话 | 学校 | 工作 |
---|---|---|---|---|
骆宏 | 26 | 1380013800 | 广东海洋大学 | 高级java开发工程师 |
对B用户,age、tel无权限查看
名字 | 年龄 | 电话 | 学校 | 工作 |
---|---|---|---|---|
骆宏 | * | * | 广东海洋大学 | 高级java开发工程师 |
编辑界面同理,直接变成disabled即可
5.2.设计实现
实现主要有如下几个步骤
5.2.1.进行数据namespace
比如下面数据
{
"name": "骆宏",
"age": 27,
"tel": "15013336**4",
"school": "广东海洋大学",
"job": "高级开发工程师"
}
借用功能权限的设计,给需要做列权限控制的数据,进行数据namespace,比如用户管理的namespace为user.module,可具备管理的key有name,age,tel,school,job
5.2.1.对数据进行授权
将用户管理(user.module)的name,age,tel,school,job访问权限授予角色A
5.2.1.获取权限上下文
将系统的所有权限,使用权限上下文抽象
上下文数据结构类似下面
{
"name": "角色A",
"用户列表": [1,2,3,4],
"功能权限列表":["user.add","user.delete"],
"行权限列表":["部门key1","部门key2","部门key3"],
"列权限列表":[{
"name": "用户管理",
"columns": "可以访问的列"
}]
}
5.2.1.后端过滤
在访问时,根据用户id,以及权限上下文,将无权限访问的key,设置为*
6.面对权限系统的变化
唯一不变的,只有变化。在建设系统时,有时候一开始并未能很直接的catch住核心业务,或者是核心业务本身也是变化的。所以在设计权限时,应该做到
- mvp原则,也就是最小可用原则
- 预留变化,比如上面提到的存储方案,是将权限数据剥离权限实体呢,还是与权限实体共存
- 保持权限的简单,不要过度预测,不要过度设计,比如RBAC模型的决策时,是否一上来就选择RBAC的扩展模型,还是先使用最简单的RBAC模型
6.权限代码的抽象
- 抽象出权限上下文
- 需要权限支持的模块,集成时统一调用权限上下文的api,切忌将权限模块的代
码污染了其他模块
DataAuthContext dataAuthContent = dataAuthContextService.getByUserId(userId);
7.写在最后
没有最完美的设计,只有最适合的设计。权限模型在建设系统时,是非常关键的一步,做好了,后面的人处理权限时,非常简单。如果做的不好,那么权限的代码就会遍布系统,遍布N个模块,重构过N个权限系统的我,苦不堪言...