如何建设一个权限系统

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等模型的本质

无论模型怎么变化,但是最终的权限落地,都是将权限授予用户。在方案选型时,要关注业务的复杂度与哪种模型的匹配度较为吻合,切勿好高骛远,过度选型。

3.6.数据库设计

表设计.png

3.7.授权界面

授权界面.png

3.8.管理界面

管理界面.png

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,设置为*

5.2.2.代码实现demo

先看个类图设计


类图设计.png

有兴趣的同学,可以copy下代码,运行下AuthEntityFactory类,suprise...该程序是一个demo,如果需要系统化,还需要进一步完善,比如将数据权限数据存储在数据库等,这部分交给有兴趣的读者去思考了哈
Column.java

public class Column {
    //列名
    public final String name;
    //是否需要权限控制
    public final boolean isLimited;

    public Column(String name, boolean isLimited) {
        this.name = name;
        this.isLimited = isLimited;
    }

    public String getName() {
        return name;
    }

    public boolean isLimited() {
        return isLimited;
    }

    @Override
    public String toString() {
        return "Column{" +
                "name='" + name + '\'' +
                ", isLimited=" + isLimited +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Column column = (Column) o;
        return isLimited == column.isLimited &&
                Objects.equals(name, column.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, isLimited);
    }
}

BaseEntity.java

public class BaseEntity {
    protected Integer id;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

AuthEntity.java

public interface AuthEntity {
    Set<Column> getColumns();

    Set<Column> getNoLimitedColumns();

    Set<Column> getLimitedColumns();
}

AuthContext.java

 public class AuthContext {
    private Map<String, Set<Column>> roleAuthMap = new HashMap<>();

    private Map<String, Set<Integer>> roleUserMap = new HashMap<>();

    public AuthContext(){
        initRoleUserMap();
        initSuperMan();
        initUserModule();
    }

    private void initRoleUserMap(){
        roleUserMap.put("user", new HashSet<>(Arrays.asList(1, 2, 3, 4)));
        roleUserMap.put("super_man", new HashSet<>(Arrays.asList(2, 3, 4, 5, 7, 8, 9)));
    }

    private void initUserModule(){
        String role = "user";
        Set<Column> columns = new HashSet<>();
        roleAuthMap.put(role, columns);
        columns.add(new Column("id", false));
        columns.add(new Column("name", false));
        columns.add(new Column("address", true));
        columns.add(new Column("tel", true));
        columns.add(new Column("age", true));
    }

    private void initSuperMan(){
        String role = "super_man";
        Set<Column> columns = new HashSet<>();
        roleAuthMap.put(role, columns);
        columns.add(new Column("id", false));
        columns.add(new Column("name", false));
        columns.add(new Column("address", false));
        columns.add(new Column("tel", false));
        columns.add(new Column("age", false));
    }

    public Set<String> getAccessableColumnNames(String roleName){
        Set<String> result = new HashSet<>();
        for (Column column : roleAuthMap.get(roleName)) {
            if(!column.isLimited){
                result.add(column.name);
            }
        }
        return result;
    }

    public boolean canAccess(Integer userId, String column){
        boolean canAccess = false;
        for(String roleName: roleUserMap.keySet()){
            if(roleUserMap.get(roleName).contains(userId)){
                canAccess = getAccessableColumnNames(roleName).contains(column);
                if(canAccess){
                    break;
                }
            }
        }
        return canAccess;
    }

    public static void main(String[] args) {
        AuthContext authContext = new AuthContext();
        System.out.println(authContext.getAccessableColumnNames("user"));
        System.out.println(authContext.getAccessableColumnNames("super_man"));
        System.out.println(authContext.canAccess(1, "name"));
        System.out.println(authContext.canAccess(2, "name"));
        System.out.println(authContext.canAccess(6, "name"));
    }
}

AuthEntityFactory.java

public class AuthEntityFactory {
    public static List<Map<String, Object>> castEntities2Map(List<BaseEntity> entities, AuthContext authContext, Integer userId) {
        List<Map<String, Object>> datas = new ArrayList<>();
        if(entities != null && !entities.isEmpty()){
            for (BaseEntity entity : entities) {
                datas.add(castEntity2Map(entity, authContext, userId));
            }
        }
        return datas;
    }

    public static Map<String, Object> castEntity2Map(BaseEntity baseEntity, AuthContext authContext, Integer userId) {
        Map<String, Object> dataMap = new HashMap<>();
        
        if (baseEntity != null) {
            Class<?> clz = baseEntity.getClass();
            try {
                //获取一个class所有的field,包含super class
                while (clz != null){
                    Field[] fields = clz.getDeclaredFields();
                    clz = clz.getSuperclass();
                    for (Field field : fields) {
                        //如果字段为columns,该字段很特殊,权限实体使用的一个保留字段,直接忽略
                        if(field.getName().equals("columns")){
                            continue;
                        }

                        //无访问权限,直接设置为*
                        if(!authContext.canAccess(userId, field.getName())){
                            dataMap.put(field.getName(), "*");
                            continue;
                        }

                        if (field.getGenericType().toString().equals("class java.lang.String")) {
                            Method m = baseEntity.getClass().getMethod("get" + getMethodName(field.getName()));
                            String val = (String) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }
                        }

                        if (field.getGenericType().toString().equals("class java.lang.Integer")) {
                            Method m = baseEntity.getClass().getMethod("get" + getMethodName(field.getName()));
                            Integer val = (Integer) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }
                        }

                        if (field.getGenericType().toString().equals("class java.lang.Double")) {
                            Method m = baseEntity.getClass().getMethod("get" + getMethodName(field.getName()));
                            Double val = (Double) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }

                        }

                        if (field.getGenericType().toString().equals("class java.lang.Boolean")) {
                            Method m = baseEntity.getClass().getMethod(field.getName());
                            Boolean val = (Boolean) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }
                        }

                        if (field.getGenericType().toString().equals("boolean")) {
                            Method m = baseEntity.getClass().getMethod(field.getName());
                            Boolean val = (Boolean) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }

                        }

                        if (field.getGenericType().toString().equals("class java.util.Date")) {
                            Method m = baseEntity.getClass().getMethod("get" + getMethodName(field.getName()));
                            Date val = (Date) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }
                        }

                        if (field.getGenericType().toString().equals(
                                "class java.lang.Short")) {
                            Method m = baseEntity.getClass().getMethod(
                                    "get" + getMethodName(field.getName()));
                            Short val = (Short) m.invoke(baseEntity);
                            if (val != null) {
                                dataMap.put(field.getName(), val);
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        
        return dataMap;
    }

    private static String getMethodName(String fildeName) throws Exception{
        byte[] items = fildeName.getBytes();
        items[0] = (byte) ((char) items[0] - 'a' + 'A');
        return new String(items);
    }

    public static void main(String[] args) {
        Student student = new Student();

        student.setAddress("广东省广州市新塘");
        student.setAge(26);
        student.setName("骆宏");
        student.setTel("1380013800");
        student.setId(10);
        AuthContext authContext = new AuthContext();

        System.out.println(castEntity2Map(student, authContext, 1));
        System.out.println(castEntities2Map(Arrays.asList(student), authContext, 1));

        System.out.println(castEntity2Map(student, authContext, 2));
        System.out.println(castEntities2Map(Arrays.asList(student), authContext, 2));

        System.out.println(castEntity2Map(student, authContext, 6));
        System.out.println(castEntities2Map(Arrays.asList(student), authContext, 6));
    }
}

Student.java

  public class Student extends BaseAuthEntity{
    private String name;
    private String tel;
    private String address;
    private Integer age;

    public Student(){
        addColumn(new Column("id", false));
        addColumn(new Column("name", false));
        addColumn(new Column("tel", true));
        addColumn(new Column("address", true));
        addColumn(new Column("age", true));
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

6.面对权限系统的变化

唯一不变的,只有变化。在建设系统时,有时候一开始并未能很直接的catch住核心业务,或者是核心业务本身也是变化的。所以在设计权限时,应该做到

  • mvp原则,也就是最小可用原则
  • 预留变化,比如上面提到的存储方案,是将权限数据剥离权限实体呢,还是与权限实体共存
  • 保持权限的简单,不要过度预测,不要过度设计,比如RBAC模型的决策时,是否一上来就选择RBAC的扩展模型,还是先使用最简单的RBAC模型

6.权限代码的抽象

  • 抽象出权限上下文
  • 需要权限支持的模块,集成时统一调用权限上下文的api,切忌将权限模块的代
    码污染了其他模块
    DataAuthContext dataAuthContent = dataAuthContextService.getByUserId(userId);

7.写在最后

没有最完美的设计,只有最适合的设计。权限模型在建设系统时,是非常关键的一步,做好了,后面的人处理权限时,非常简单。如果做的不好,那么权限的代码就会遍布系统,遍布N个模块,重构过N个权限系统的我,苦不堪言...

谢谢大家看到最后,Thank you for you reading
参考链接:功能权限的设计
数据权限的设计

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

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,938评论 2 89
  • 这是5月4号凌晨两点多在家属院拍的。 5月3号加班到凌晨两点多,太困了,在办公室打开折叠床躺下就睡着了...
    良良8209阅读 363评论 0 1
  • 在本节刻意练习主要是在学习,生活中,我们要能够区分自己的强与弱点,在强的点上,我们要学会“逃离”即跳出舒适圈。重点...
    千寻001阅读 231评论 0 0
  • 刷微博的时候看到了一新闻,某组织将中年危机的年龄划分到了1992年,对于92年生人的本座来说这无疑像是当头棒喝,瞬...
    The_Christmas阅读 118评论 0 1