美文网首页
如何设计权限系统

如何设计权限系统

作者: 周群力 | 来源:发表于2019-08-17 17:29 被阅读0次

    # 权限系统的数据模型

    模型图

    常用的数据模型如图,设计角色的目的是为了方便批量管理用户的权限;设计权限码的目的是避免角色的权限改变时需要改代码,比如网上的一个例子[1],假如代码写成了这样:

    代码基于角色判断权限

    如果某天项目管理员的权限有变,或者有新的角色有权限使用该资源,代码就要改动:

    如果部门管理员也有该资源的权限了

    出现这种问题的原因是:“角色”和“资源”的关系是易变的,不应该把易变的东西写在代码里。为解决这种问题,可以抽象出“权限码”的概念,权限码和资源的关系是不易变的,可以放心写在代码里,如shiro的设计[2]:

    # 设计点:如何描述权限码和资源的关系

    a. 在访问资源的代码里声明“我需要哪些权限码”

    例如:

    void updateSystem(){

        user.checkPermission("system:update");

        //do something

    }

    又如注解式声明:

    @Permission("system:update")

    void updateSystem(){

         //do something

    }

    这种方式的缺点是:

    1. 只能采用匹配权限码的策略,不灵活。比如想实现system:*这种权限码(代表拥有system这个资源的所有权限),就要在system资源相关的所有方法的注解里加上"system:*"这串权限码

    2. 只支持接口粒度权限控制,支持不了数据粒度权限控制

    b.更进一步:权限码自描述,支持通配符

    参考shiro的设计[2]:

    role61=*:view

    void listUsers(){

        subject().checkPermissions("user:view");  //成功

        // do something

    }

    优点
    1. 支持通配符;权限码可读性好

    缺点:

    1. 只支持接口粒度权限控制,支持不了数据粒度权限控制

    c.再进一步:权限码自描述,支持数据规则

    假如我们有一个修改配置的updateConfig方法,想实现“每个bu的用户只能修改本bu的配置”,那么可以将权限码设计的更加自描述一点:

    @Resource("config:update")    //可以把updateConfig方法也抽象的认为是一种“资源”

    void updateConfig(Config config,@Factor String bu){

    }

    role零售BU管理员=config:update:(bu=retail)

    role影视BU管理员=config:update:(bu=movie)

    role零售和影视BU管理员=config:udpate:(bu in {retail,movie})

    role集团管理员=config:update:(bu=*)

    假如一个零售BU的新用户想使用系统,赋予他“零售BU管理员”的角色,就能拥有本BU权限,同时还不会越权修改其他BU的数据。

    当这名管理员访问updateConfig方法时,框架会查出他的权限码,计算权限码里跟updateConfig方法相关的规则,最终判断他有没有权限访问。

    (注:仔细想想,这个案例可以理解为:用规则表达式来描述“角色”和“权限码”之间的关系,即

    role零售BU管理员=config:update:(bu=retail)    可以理解为  角色=权限码:数据规则    

    优点:

    1. 支持数据粒度的权限控制

    # 设计点:如何设计数据权限?

    a. 在业务代码中做入参校验,校验用户是否有该入参的权限

    使用注解的声明式校验:

    @Resource("config:update") 

    void updateConfig(Config config,@Factor String bu){

    }

    比如上述方法,因为该方法访问数据时sql语句会带上where bu=xxx字段,所以需要校验,在方法入参里用@Factor标记出需要对bu字段做校验

    又或者注解+类的声明式校验:

    @Checker("BuChecker.class")

    @Resource("config:update")

    void updateConfig(Config config,String bu){

    }

    class BuChecker implements IChecker{

        boolean check(){

            request.getParameter("bu");

            //检查

            return true;

        }

    }

    再比如显示的检查,命令式校验

    @Resource("config:update")

    void updateConfig(Config config,String bu){

        List<String> buList=getUserBu();

        if(!buList.contains(bu)){

            throw new IllegalArgumentException("blabla")

        }

    }

    又或者把上述这段逻辑封装成工具类/父类方法,业务代码中调用AuthorityUtils.assertUserBu(bu,"无权访问!");

    b. 对业务代码无侵入:SQL层做拦截

    听说用mybatis拦截器可以做,向大佬请教后了解到大致的思路如下(顺便推荐下大佬博客https://blog.hihuacheng.com/blog/open/article/8

    用mybaties拦截器对最终执行的sql进行执行前修改,根据配置的权限规则把各种业务翻译成对应的不同sql子句,再把sql子句合并到原始sql语句里。
    举个例子,假如有一个新增产品的方法需要做权限控制,xml里sql写成:

    insert (部门,name) into  product

    values

    (@{dept,cacheId=dept666},${name})

    mybaties拦截器拦截到sql后,匹配占位符@{xxx},匹配到之后去读cache里id为dept666的规则,比如规则配置成:

    dept=DepartmentService.getDepart()

    读到规则之后通过反射把这句代码执行(调用spring bean),得到depart这个字段的值是"零售",然后替换sql语句,最终sql变成:

    insert (部门,name) into  product

    values

    ("零售","大力丸")

    这是insert的例子,select语句可以用占位符实现,也可以自动给原始sql加一层括号,比如:

    select * from (select * from ..) where 部门=零售

    c. 关键sql条件不通过入参获取,靠业务代码在SQL where语句里加上判断条件

    比如需求是用户只能修改自己拥有权限的部门的数据,那么部门字段不通过入参或取,而是由updateProduct(productId,product)这个方法先查用户拥有权限的部门,之后拼SQL:

    update product set ....

    where    productId=.....  and 部门 in (零售,广告)  //这里拼上用户拥有权限的部门

    # 设计点:如何把权限和组织机构管理相结合?

    可以加入用户组这层抽象[3],如

    # 附:引文

    [1] RBAC新解:基于资源的权限管理(Resource-Based Access Control)https://globeeip.iteye.com/blog/1236167

    [2]授权——《跟我学Shiro》https://jinnianshilongnian.iteye.com/blog/2020017

    [3]ITeye论坛关于权限控制的讨论 https://www.iteye.com/magazines/82

    相关文章

      网友评论

          本文标题:如何设计权限系统

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