本文编写主要目的是为了记录工作中的一些编程思想和细节,以便后来查阅。
1. 概述
先来介绍下标题的内容的含义(标题太长,估计很多人也不知道具体讲的是什么,而且JEECG系统不是自带权限管理吗, 还要改造什么?)。
其实, 本文讲述的权限管理框架,是一套简化开发流程的开发框架(是给开发人员用,针对代码层面的),它是基于mybatis的getMapper方法中动态代理的思想,衍生出来的一套框架。而不是给用户使用的用户权限管理系统。
废话不多说,我通过项目中小伙伴实际遇到的问题,来介绍本框架的由来和内容。
2. 项目中坑爹的需求
本项目是一个后台管理系统(基于JEECG, 烂大街的那种)。在项目中, 领导提出需求,
人员信息页面:
系统管理员要显示全部内容;
机构管理员仅能看它机构下面的内容;
部门主管只能看他下属员工;
普通员工只能看自己的信息
然后接下来就是页面A,B,C.....到不知道几,所有页面都要根据不同角色来做数据过滤。然后要在一两个月内完成,然后做项目的小伙伴直接蒙圈了,这尼玛几个月能搞定???但是没有办法,谁让是领导的任务呢,然后他开始第一个页面的实现,吭哧吭哧的开始敲代码,伪代码如下:
public class PersonInfoController{
public String getPersonInfo() {
User user = getUser(); //去数据库查询用户信息
String role = user.getRole(); //获取角色信息
if ("系统管理员".equals(role)){
return getPersonAdminInfo();//数据库SQL获取系统管理员角色的人员信息
} else if ("机构管理员".equals(role)){
return getJGAdminInfo("厦门机构"); //数据库SQL获取机构管理员角色的人员信息
}
.....//以下省略其他角色的信息
}
}
getPersonAdminInfo 对应的SQL : SELECT * FROM user;
getJGAdminInfo 对应的SQL : SELECT * FROM user WHERE departId='厦门机构'
....省略其他角色的SQL信息
然后,我们发现代码开始越来越冗余, SQL越来越多,也不知道哪个SQL是哪个角色的。需要从代码开始部分开始看,才能确定一个SQL是对应哪个角色的哪个功能。
最后,我总结上述代码存在以下几个问题:
1. 只要是涉及角色不同的地方, 都要手动去获取角色role信息, 增加许多不必要的代码逻辑。且判断的逻辑很容易出错(不同名字很容易拼写错误;或者if 判断的时候,写的是系统管理员的逻辑,结果if里面是机构管理员的判断)
2. 角色一多, 代码量就多, 代码冗余, 可阅读性差, 代码耦合性高。 即领导说,我要在哪个页面增加一个角色。 那这个小伙伴只能吭哧吭哧的在众多else if中增加一个else if代码。
3. 如果不同页面,不同角色的执行优先级调整, 就需要修改原来的代码逻辑,(即一个人即是机构管理员,又是部门主管,原先A页面,是如果你是机构管理员就优先返回机构原理员的信息,如果不是,就返回部门主管的信息。 需求变动,该页面你要优先判断他是部门主管,然后再判断它是机构管理员)然后吭哧吭哧的看if else代码,然后找到位置, 然后把部门管理员的if else挪到机构管理员的if else上面,严重破坏了设计模式的开闭原则。
4. 最主要的是,也是我设计该框架的初衷。 对于很多角色来说,它们的主要SQL都是一样的,例如人员信息页面,都是查询USER表, 就是后面的查询条件不同。例如系统管理员查询全表, 机构管理员增加WHERE departId=‘厦门机构’, 普通人员则是 WHERE ID='我自己的用户ID'。
3. 框架介绍
因此,基于以上因素,设计了一个实现用户权限的开发框架。话不多说,直接上代码
public class AuthFilterTestDemo {
@Test
public void AuthFilterTest(){
AuthFilterUtils authFilterUtils = new AuthFilterUtils();
String result = authFilterUtils.xtHandler(()->{
//处理管理员的逻辑
...省略具体逻辑处理
}).jgHandler(()->{
//处理机构管理员的逻辑
...省略具体逻辑处理
}).putongHanler(()->{
//处理普通用户的逻辑
...省略具体逻辑处理
}).getResult();
}
}
通过以上代码,完成人员信息,不同角色的人员信息获取。
首先, 你只需要声明一个AuthFilterUtils, 然后通过JAVA8的匿名函数来指定不同角色的处理逻辑就,最后通过getResult获取结果就可以了。同时,如果不同角色之间的的逻辑有重叠,可以使用addDefaultHandler方法来实现代码复用(这种方法default的执行优先级是最低的,开发的时候需要考虑)
public class AuthFilterTestDemo {
@Test
public void AuthFilterTest(){
AuthFilterUtils authFilterUtils = new AuthFilterUtils();
String result = authFilterUtils.xtHandler(()->{
//处理管理员的逻辑
...省略具体逻辑处理
}).jgHandler(()->{
//处理机构管理员的逻辑
...省略具体逻辑处理
}).addDefaultHandler(()->{
//处理默认的逻辑,即其他已经定义过的角色都不满足的情况下,调用该方法
...省略具体逻辑处理
}).getResult();
}
}
或者使用otherRoleHandler方法来实现(该方法可以实现在架构中未出现过的角色信息,防止领导给你突然袭击,临时加一个从来没有出现过的角色),以下代码普通角色和机构管理员是同一个处理逻辑。
public class AuthFilterTestDemo {
@Test
public void AuthFilterTest(){
AuthFilterUtils authFilterUtils = new AuthFilterUtils();
String result = authFilterUtils.xtHandler(()->{
//处理管理员的逻辑
...省略具体逻辑处理
}).otherRoleHandler(()->{
//处理默认的逻辑,即其他已经定义过的角色都不满足的情况下,调用该方法
...省略具体逻辑处理
}, "普通角色", "机构管理员").getResult();
}
}
另外,基于上面的问题3, 说一个页面不同角色拥有不同优先级, 怎么处理呢?
AuthFilterUtils方法提供了sortedRoles方法, 支持默认角色重排序, 你只要告诉框架,你理想的处理顺序是什么。
public class AuthFilterTestDemo {
@Test
public void AuthFilterTest(){
AuthFilterUtils authFilterUtils = new AuthFilterUtils();
String result = authFilterUtils.xtHandler(()->{
//处理管理员的逻辑
...省略具体逻辑处理
getUserInfo(null,null);
}).jgHandler(()->{
//处理机构管理员的逻辑
...省略具体逻辑处理
getUserInfo('厦门机构',null);
}).otherRoleHandler(()->{
//处理默认的逻辑,即其他已经定义过的角色都不满足的情况下,调用该方法
...省略具体逻辑处理
getUserInfo(null,'我自己的用户ID');
}, "putong")
.sortedRoles("系统管理员", "普通用户","机构管理员").getResult();
}
}
看到这里,是不是发现代码清晰了很多, 再也不用费劲心思的从头开始看else if方法, 找对应角色的具体方法在哪里。也不同手动去判断当前用户角色, 调整角色执行顺序, 你只要告诉框架,不同角色你要怎么处理就OK了。 小伙伴表示很开心。
以下是项目中AuthFilterUtils的代码框架,小伙伴们可以参考下
private Map<String, AuthFilterInterface> dicts = new HashMap<>();
private List<String> otherRoles = new ArrayList<>();
private AuthFilterInterface defaultHandler;
private String[] sortedRoles = new String[]
{"系统管理员", "机构管理员","普通用户"};//不同角色信息,默认角色执行顺序
public AuthFilterUtils sortedRoles(String... roleList) {
Optional.ofNullable(roleList).ifPresent((var)->{
this.sortedRoles = var;
});
return this;
}
public AuthFilterUtils addDefaultHandler(AuthFilterInterface t) {
defaultHandler = t;
return this;
}
public AuthFilterUtils adminHandler(AuthFilterInterface t) {
dicts.put(DD.Role_Code.ADMIN, t);
return this;
}
....省略其它角色的handler方法
public String getResult() {
User user = getUser();//伪代码,小伙伴们可以根据需求自己实现
String role = user.getRole();
//判断当前角色是否为系统设定角色
for(int index = 0; index < this.sortedRoles.length; index++) {
if (roleCode.contains(this.sortedRoles[index]) &&
dicts.get(this.sortedRoles[index]) != null ) {
return dicts.get(this.sortedRoles[index]).accept();
}
}
//判断当前角色是否为其他角色
for (String otherRole : otherRoles) {
if (roleCode.contains(otherRole)) {
return dicts.get(otherRole).accept();
}
}
//判断是否使用默认处理逻辑
if(defaultHandler != null) {
return defaultHandler.accept();
}
throw new BusinessException("error set roles, please check the user[" +
session.getAttribute("userName") + "]");
}
.....以下是匿名方法的接口定义
@FunctionalInterface
public interface AuthFilterInterface {
String accept();
}
4. SQL统一化处理
到这里, 我们已经完成了大部分的用户权限管理的开发框架内容。但是, 回头看下,有人发现不是不同角色还是要写不同的SQL和对应的SQL方法吗? 然后在不同角色的代码方法体里面调用, 相当于N个角色,一个功能SQL要写N次。即使SQL之前的代码很相似,只是一个查询条件的不同。
getPersonAdminInfo 对应的SQL : SELECT * FROM user;
getJGAdminInfo 对应的SQL : SELECT * FROM user WHERE departId='厦门机构'
....省略其他角色的SQL信息
我们部门的小伙伴想了想,心里很无奈,表示还是算了吧,controller层已经做了角色过滤了,减少了不少工作量了,DAO层的sql还是自己写吧,大不了多花点时间写几个冗余文件就是了。
表示对代码有追求的我,那怎么可以忍受,表示controller层都改造了,也不差mapper层了。 直接上代码
@LockMapper
public interface UserMapper {
@LockSelect(
mSQL = "SELECT * FROM user",
xtSQL= "",//系统管理员查询全部用户
jgSQL= "WHERE departId=#{departId}",
putongSQL="WHERE id=#{userId}"
)
List<UserEntity> getUser(@LockParam("departId") String departId,
@LockParam("userId") String userId);
}
代码风格模仿的mybatis,@LockMapper表示该接口是mapper文件, @LockSelect表示该方法是查询方法。
其中mSQL表示主函数, xtSQL表示系统管理员SQL, jgSQL表示机构管理员SQL, putongSQL表示普通人员SQL。
如果机构管理员查找人员信息,那么就可以得到 SELECT * FROM user WHERE departId=#{departId};
如果普通人员查找人员信息, 那么就可以得到SELECT * FROM user WHERE id=#{userId}
接下来就是实际使用了,看下面代码:
public class AuthFilterTestDemo {
@Test
public void AuthFilterTest(){
AuthFilterUtils authFilterUtils = new AuthFilterUtils();
Object result = authFilterUtils.xtHandler(()->{
//处理管理员的逻辑
//lockMapperRegistry 通过spring 依赖注入
UserMapper mapper = lockMapperRegistry.getMapper(UserMapper.class);
return mapper.getUserInfo(null,null);
}).jgHandler(()->{
//处理机构管理员的逻辑
//lockMapperRegistry 通过spring 依赖注入
UserMapper mapper = lockMapperRegistry.getMapper(UserMapper.class);
return mapper.getUserInfo('厦门机构',null);
}).otherRoleHandler(()->{
//处理默认的逻辑,即其他已经定义过的角色都不满足的情况下,调用该方法
...省略具体逻辑处理
//lockMapperRegistry 通过spring 依赖注入
UserMapper mapper = lockMapperRegistry.getMapper(UserMapper.class);
return mapper.getUserInfo(null,'我的用户ID');
}, "putong).getResult();
}
}
跟AuthFilterUtils一样,LockSelect也提供了 其他角色SQL 以及 角色执行顺序调整 的方法。
@LockMapper
public interface UserMapper {
@LockSelect(
mSQL = "SELECT * FROM user",
xtSQL= "",//系统管理员查询全部用户
jgSQL= "WHERE departId=#{departId}",
otherRoleSQL={
"WHERE id=#{userId}",
"WHERE id2=#{userId}"},
otherRoleName={"putong1","putong1"}, //otherRoleName和otherRoleSQL一一对应
roleOrders={"机构管理员","系统管理员","putong"}
)
List<UserEntity> getUser(@LockParam("departId") String departId,
@LockParam("userId") String userId);
}
otherRoleSQL和otherRoleName一一对应,即
putong1角色的SQL为: SELECT * FROM user WHERE id=#{userId},
putong2角色的SQL为:SELECT * FROM user WHERE id2=#{userId}
主要核心文件是LockMapperRegistry和MapperProxy文件。 通过LockMapperRegistry文件getMapper方法得到对应mapper文件的动态代理对象(一个MapperProxy的实例),然后代理对象mapper调用对应方法, 动态代理拦截,调用动态代理方法,在代理方法中获取注解的SQL, 并根据用户角色,拼接SQL, 得到最终执行SQL,并在mapper代理中实现SQL的执行和获取。
由于涉及核心代码部分,故不公开分析源码。
简单讲下思想吧:
1. 在LockMapperRegistry文件中定义一个@PostConstruct, 实现启动时,读取系统中的mapper文件(开发时候写SQL代码), 并组装成对应的mapperProxy代理对象, 存到一个map里面(将接口作为key, mapperProxy作为value)。
2. 在调用的时候, 通过getMapper方法,从map中取到最终的动态代理对象, 然后通过Proxy.newProxyInstance(interface.getClassLoad, new Class[]{interface}, mapperProxy) 动态代理,实现对应maper文件的动态代理。
3. 然后在mapperProxy类中的invoke方法中, 获取方法上的注解信息, 然后使用之前的AuthFilterUtils方法,拼接SQL,完成SQL组装,查询以及后期字段映射工作。