权限之数据权限

概念

无论为数据操作赋予怎样的业务含义,其本质上仍然是数据的增删改查操作(如下图)。

image.png

随着业务的演进,逐渐衍生出精细化管理数据的诉求。我遇到的业务场景是在企业级数据管理中,对不同职级的员工展示不同的数据。我的业务上的诉求是对SELECT进行权限控制,对INSERTUPDATEDELETE没有权限限制要求。

数据权限实现的复杂度还是较高的,在叙述实现之前,我们先预设期望的结果: 能够将繁琐的细节都封装在内部逻辑中,对外部提供统一的接口调用。

相关设计理念可以参考我之前写的文章,《外观模式(封装交互,简化调用)》。

在这个模型中,我们可选切入点有:

  • 用户层面进行业务逻辑判断(不推荐)
  • SQL层面上的抽象
  • 数据库视图(不推荐)

我在这里选择了使用SQL来完成数据权限的实现,通过SQL的组装来完成宽泛的数据权限的控制。

原型实现

背景:某超市拥有员工 5 名,其组织架构如下图。该小超市的信息化程度极高,已经拥有完备的移动版的CRM系统。

权限之数据权限_第1张图片
image.png

诉求:

  • 店长可以看到所有的销售数据;
  • 营业员可以看到自己的销售数据,但是不能看到别人的销售数据;
  • 收银出纳可以看到所有人的销售数据;
  • 采购库管不能看到销售数据;

先贴上原型实现,说明流程:

// 规则对象用于定义规则
// 这些规则包括用户定义和管理员定义
// 规则应可以序列化(此处省略)
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

实现步骤拆分

  • 组织树
  • 人与组织树
  • 角色与功能权限
  • 角色与数据权限
  • 角色与人
  • 应用权限规则

组织树

权限之数据权限_第2张图片
image.png

通常业务限定组织树的深度都不会过高,一般在5层以内。实现组织树的方式有多种:

  • 递归方式
  • 前序遍历树,参考无限级分类实现思路 (组织树的分级管理)

定义组织树目的是承载人的容器,通过将人分配到对应组织中完成上下文的联系。

权限之数据权限_第3张图片
image.png

通过将人分配到不同的部门中,即完成了人与组织的关系。这样我们就能通过上下文推导出人所具备的资源。

通过对组织的资源限制即可完成预初始化状态的SQL配置,如:

// 大老板
SELECT * FROM table;
// 李妲
 SELECT * FROM table WHERE department in ('行政部','销售部');
// 李达
 SELECT * FROM table WHERE department = '行政部';
// 肖雨
 SELECT * FROM table WHERE store = '六盘水...;

解题步骤

* 添加组织(建设组织树)
* 添加人
* 添加功能权限
* 分配角色到功能权限
* 添加数据权限
* 分配角色到数据权限
* 分配人到组织
* 分配人到角色

目前整个数据权限管理流程已经做通了,具体的代码涉及业务细节就不贴出来了。
如果有各种疑问,欢迎提问。

你可能感兴趣的:(权限之数据权限)