如何建设一个权限系统

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 getColumns();

    Set getNoLimitedColumns();

    Set getLimitedColumns();
}

AuthContext.java

 public class AuthContext {
    private Map> roleAuthMap = new HashMap<>();

    private Map> 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 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 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 getAccessableColumnNames(String roleName){
        Set 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> castEntities2Map(List entities, AuthContext authContext, Integer userId) {
        List> datas = new ArrayList<>();
        if(entities != null && !entities.isEmpty()){
            for (BaseEntity entity : entities) {
                datas.add(castEntity2Map(entity, authContext, userId));
            }
        }
        return datas;
    }

    public static Map castEntity2Map(BaseEntity baseEntity, AuthContext authContext, Integer userId) {
        Map 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
参考链接:功能权限的设计
数据权限的设计

你可能感兴趣的:(如何建设一个权限系统)