权限之数据权限

概念

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

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 = '六盘水...;

解题步骤

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容