概念
无论为数据操作赋予怎样的业务含义,其本质上仍然是数据的增删改查操作(如下图)。
随着业务的演进,逐渐衍生出精细化管理数据的诉求。我遇到的业务场景是在企业级数据管理中,对不同职级的员工展示不同的数据。我的业务上的诉求是对SELECT
进行权限控制,对INSERT
、UPDATE
、DELETE
没有权限限制要求。
数据权限实现的复杂度还是较高的,在叙述实现之前,我们先预设期望的结果: 能够将繁琐的细节都封装在内部逻辑中,对外部提供统一的接口调用。
相关设计理念可以参考我之前写的文章,《外观模式(封装交互,简化调用)》。
在这个模型中,我们可选切入点有:
- 用户层面进行业务逻辑判断(不推荐)
-
SQL
层面上的抽象 - 数据库
视图
(不推荐)
我在这里选择了使用SQL
来完成数据权限的实现,通过SQL
的组装来完成宽泛的数据权限的控制。
原型实现
背景:某超市拥有员工 5 名,其组织架构如下图。该小超市的信息化程度极高,已经拥有完备的移动版的CRM
系统。
诉求:
- 店长可以看到所有的销售数据;
- 营业员可以看到自己的销售数据,但是不能看到别人的销售数据;
- 收银出纳可以看到所有人的销售数据;
- 采购库管不能看到销售数据;
先贴上原型实现,说明流程:
// 规则对象用于定义规则
// 这些规则包括用户定义和管理员定义
// 规则应可以序列化(此处省略)
public class RuleDataVO {
// 受众群体
private String audienceGroup;
// 规则实体
private String rule;
public RuleDataVO(String audienceGroup, String rule) {
this.audienceGroup = audienceGroup;
this.rule = rule;
}
public String getAudienceGroup() {
return audienceGroup;
}
public void setAudienceGroup(String audienceGroup) {
this.audienceGroup = audienceGroup;
}
public String getRule() {
return rule;
}
public void setRule(String rule) {
this.rule = rule;
}
}
public class SaleDataVO {
private Long userId;
private Long storeId;
private String goodName;
private Integer goodPrice;
public SaleDataVO(Long userId, Long storeId, String goodName, Integer goodPrice) {
this.userId = userId;
this.storeId = storeId;
this.goodName = goodName;
this.goodPrice = goodPrice;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getGoodName() {
return goodName;
}
public void setGoodName(String goodName) {
this.goodName = goodName;
}
public Integer getGoodPrice() {
return goodPrice;
}
public void setGoodPrice(Integer goodPrice) {
this.goodPrice = goodPrice;
}
@Override
public String toString() {
return goodName + ":" + goodPrice / 100.0;
}
}
// 用于模拟数据库操作
public class MockDataSource {
// 模拟销售数据库表
public static class SaleDataTable {
private static List list = new ArrayList<>();
static {
list.add(new SaleDataVO(1L, 1L, "贝因美奶粉", 9800));
list.add(new SaleDataVO(1L, 1L, "毛巾", 2810));
list.add(new SaleDataVO(2L, 1L, "黑人牙膏", 3200));
}
public static List retrieval(Long userId, RuleDataVO ruleDataVO) {
String ruleContent = ruleDataVO.getRule();
if (ruleContent != null) {
if (Objects.equals("自身", ruleContent)) {
// 如果检查是营业员
if (userId == 1 || userId == 2) {
return filter(userId);
} else {
return filter(null);
}
} else if (Objects.equals("本门店内", ruleContent)) {
return filter(1L, 2L);
} else if (Objects.equals("无", ruleContent)) {
return filter(null);
}
}
return filter(null);
}
private static List filter(Long... userIds) {
List saleDataVOS = new ArrayList<>();
if (userIds == null) return saleDataVOS;
for (SaleDataVO saleDataVO : list) {
for (Long userId : userIds) {
if (saleDataVO.getUserId().equals(userId)) {
saleDataVOS.add(saleDataVO);
break;
}
}
}
return saleDataVOS;
}
}
// 模拟规则库表
public static class RuleTable {
private static List list = new ArrayList<>();
static {
list.add(new RuleDataVO("营业员", "自身"));
list.add(new RuleDataVO("店长", "本门店内"));
list.add(new RuleDataVO("收银出纳", "本门店内"));
list.add(new RuleDataVO("采购库管", "无"));
}
public static void add(RuleDataVO dataVO) {
list.add(dataVO);
}
public static RuleDataVO retrieval(String audienceGroup) {
for (RuleDataVO dataVO : list) {
if (dataVO.getAudienceGroup().equalsIgnoreCase(audienceGroup)) {
return dataVO;
}
}
return null;
}
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MockDataSource.SaleDataTable.retrieval(1L, MockDataSource.RuleTable.retrieval("营业员")));
System.out.println(MockDataSource.SaleDataTable.retrieval(2L, MockDataSource.RuleTable.retrieval("营业员")));
System.out.println(MockDataSource.SaleDataTable.retrieval(3L, MockDataSource.RuleTable.retrieval("店长")));
System.out.println(MockDataSource.SaleDataTable.retrieval(4L, MockDataSource.RuleTable.retrieval("收银出纳")));
System.out.println(MockDataSource.SaleDataTable.retrieval(5L, MockDataSource.RuleTable.retrieval("采购库管")));
}
}
// 调用结果:
[贝因美奶粉:98.0, 毛巾:28.1]
[黑人牙膏:32.0]
[贝因美奶粉:98.0, 毛巾:28.1, 黑人牙膏:32.0]
[贝因美奶粉:98.0, 毛巾:28.1, 黑人牙膏:32.0]
[]
在这个原型上省略了不必要的复杂性(如DB操作,业务操作),仅关注规则的定义与解析过程。
原型上简单定义了自身
本门店内
无
的语法规则,结合上下文判拼接处正确的SQL
语句。
我理解的权限控制核心就在这里:定义语法规则解析并应用到SQL规范中。
- 后端上定义语法规则,预初始化入库,即完成数据权限的控制。
- 前端上定义语法规则(需考虑SQL注入问题),即时操作入库,即完成数据权限的控制;
上述是个非常简单的原型,说明了解题思路但是实际的可操作性不高。因此我们需要接着对它进行抽象。
抽象
指令:查询当天的销售数据;
环境:基于上下文参数推断出所属的资源,如:所属的公司、部门等;
权限:仅自身相关的数据、本部门内、本部门及下属部门、所有、无;
对象 | 环境 | 权限 | SQL |
---|---|---|---|
营业员 | 好又多超市101分店 | 仅自身相关的数据 | uid=$uid |
收银出纳 | 好又多超市101分店 | 本部门及下属部门 | uid in $uids |
采购库管 | 好又多超市101分店 | 无 | uid = null |
店长 | 好又多超市101分店 | 本部门及下属部门 | uid in $uids |
实现步骤拆分
- 组织树
- 人
- 人与组织树
- 角色与功能权限
- 角色与数据权限
- 角色与人
- 应用权限规则
组织树
通常业务限定组织树的深度都不会过高,一般在5层以内。实现组织树的方式有多种:
- 递归方式
- 前序遍历树,参考无限级分类实现思路 (组织树的分级管理)
定义组织树目的是承载人的容器,通过将人分配到对应组织中完成上下文的联系。
通过将人分配到不同的部门中,即完成了人与组织的关系。这样我们就能通过上下文推导出人所具备的资源。
通过对组织的资源限制即可完成预初始化状态的SQL配置,如:
// 大老板
SELECT * FROM table;
// 李妲
SELECT * FROM table WHERE department in ('行政部','销售部');
// 李达
SELECT * FROM table WHERE department = '行政部';
// 肖雨
SELECT * FROM table WHERE store = '六盘水...;
解题步骤
* 添加组织(建设组织树)
* 添加人
* 添加功能权限
* 分配角色到功能权限
* 添加数据权限
* 分配角色到数据权限
* 分配人到组织
* 分配人到角色
目前整个数据权限管理流程已经做通了,具体的代码涉及业务细节就不贴出来了。
如果有各种疑问,欢迎提问。