美文网首页技术
权限之数据权限

权限之数据权限

作者: 幺鹿 | 来源:发表于2018-05-08 21:13 被阅读2351次

    概念

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

    image.png

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

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

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

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

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

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

    原型实现

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

    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<SaleDataVO> 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<SaleDataVO> 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<SaleDataVO> filter(Long... userIds) {
                List<SaleDataVO> 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<RuleDataVO> 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

    实现步骤拆分

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

    组织树

    image.png

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

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

    image.png

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

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

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

    解题步骤

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

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

    相关文章

      网友评论

        本文标题:权限之数据权限

        本文链接:https://www.haomeiwen.com/subject/iaiwrftx.html