# 权限系统的数据模型
模型图常用的数据模型如图,设计角色的目的是为了方便批量管理用户的权限;设计权限码的目的是避免角色的权限改变时需要改代码,比如网上的一个例子[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
网友评论