Mybatis-plus数据权限实现方案
前一篇
Mybatis-plus数据权限DataPermissionInterceptor实现
前言
上篇中,会发现,注解是放在mapper的方法上的,所以不能使用Mybatis-plus自带的17个方法,只能自己手写。而且针对于删改查3个功能,需要写3个查询方法,只是注解的操作类型不同,每一个要控制权限的对象都需要额外定义查询,都是重复工作量。
一、实现思路
1、自己实现Mybatis-plus的BaseMapper,增加用于 删、改、查 权限的默认查询,不用每个对象都定义一遍
2、DataPermissionHandler中根据mappedStatementId直接判断判断默认的查询,区分出删、改、查 权限,拼接上对应的权限sql
3、这样不需要写mapper的方法了,怎么区分控制什么权限呢,总不能也根据包名+类名确定吧,所以可以在mapper上加上自定义注解
二、实现细节
1.加强Mybatis-plus的BaseMapper
就三步
- 自定义全局方法类,需继承com.baomidou.mybatisplus.core.injector.AbstractMethod
- 注册,将自定义的全局方法注册到mp,继承com.baomidou.mybatisplus.core.injector.DefaultSqlInjector
- 定义自己的BaseMapper,继承原本的BaseMapper
-
自定义全局方法类
官方样例这样写的public class DeleteAll extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { /* 执行 SQL ,动态 SQL 参考类 SqlMethod */ String sql = "delete from " + tableInfo.getTableName(); /* mapper 接口方法名一致 */ String method = "deleteAll"; SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addDeleteMappedStatement(mapperClass, method, sqlSource); } }
这一步是核心,在这里定义的方法名、sql语句。我们要增加的是根据id查询,所以我们直接继承本身的SelectById,然后把方法改成我们自定义的方法名就行了,先看看SelectById咋实现的,SelectById类如下
public class SelectById extends AbstractMethod { public SelectById() { super(SqlMethod.SELECT_BY_ID.getMethod()); } public SelectById(String name) { super(name); } public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; SqlSource sqlSource = new RawSqlSource(this.configuration, String.format(sqlMethod.getSql(), this.sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class); return this.addSelectMappedStatementForTable(mapperClass, this.getMethod(sqlMethod), sqlSource, tableInfo); } }
可以看到里面有两个构造方法,官方的样例没有这个,咱们也把他忽略,直接看injectMappedStatement。比官方的样例的DeleteAll多了个SqlMethod类,点进去看看这个是啥,如下
public enum SqlMethod { INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>"), ... SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"), ... private final String method; private final String desc; private final String sql; // getter }
就是个枚举类,定义了方法名、sql结构。
SqlMethod被使用两次,sqlMethod.getSql()和this.getMethod(sqlMethod),
sqlMethod.getSql()是获取sql,this.getMethod(sqlMethod)是获取方法名,所以我们直接照搬过来,将this.getMethod(sqlMethod)替换成自己的方法名即可。我们要增加selectByIdForFind、selectByIdForEdit、selectByIdForRemove三个方法
所以新增类SelectByIdForEdit,继承自SelectById,把SelectById的injectMappedStatement方法直接复制过来,加上个@Override,然后把this.getMethod(sqlMethod)换成"selectByIdForFind"就行了,我是把"selectByIdForFind"放到一个常量类里去了public class SelectByIdForEdit extends SelectById { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; SqlSource sqlSource = new RawSqlSource(this.configuration, String.format(sqlMethod.getSql(), this.sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class); return this.addSelectMappedStatementForTable(mapperClass, SqlMethodConstants.SELECT_BY_ID_FOR_EDIT, sqlSource, tableInfo); } }
-
注册
直接复制的样例里的,把刚刚自己定义的方法类加入进去public class MyLogicSqlInjector extends DefaultSqlInjector { /** * 如果只需增加方法,保留MP自带方法 * 可以super.getMethodList() 再add * @return */ @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); methodList.add(new SelectByIdForEdit()); methodList.add(new SelectByIdForFind()); methodList.add(new SelectByIdForRemove()); return methodList; } }
最后要将这个注入到spring里,定义一个configuration
@Configuration public class MybatisPlusConfiguration { @Bean public MyLogicSqlInjector myLogicSqlInjector() { return new MyLogicSqlInjector(); } }
-
定义自己的BaseMapper
public interface MyBaseMapper<T> extends BaseMapper<T> { T selectByIdForFind(Serializable id); T selectByIdForEdit(Serializable id); T selectByIdForRemove(Serializable id); }
2.调整DataPermissionHandler
代码如下:
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
try {
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
Method[] methods = clazz.getDeclaredMethods();
//判断mapper上是否有注解
CustomDataPermission classAnnotation = clazz.getAnnotation(CustomDataPermission.class);
if (classAnnotation == null) {
return where;
}
if (methodName.equals(SqlMethodConstants.SELECT_BY_ID_FOR_FIND) || methodName.equals(SqlMethodConstants.SELECT_BY_ID_FOR_EDIT) || methodName.equals(SqlMethodConstants.SELECT_BY_ID_FOR_REMOVE)) {
return handle(classAnnotation, where, methodName);
} else {
//保留方法上的自定义注解
for (Method method : methods) {
if (!methodName.equals(method.getName())) {
continue;
}
// 获取自定义注解
CustomDataPermission annotation = method.getAnnotation(CustomDataPermission.class);
if (annotation == null) {
continue;
}
return handle(annotation, where, methodName);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return where;
}
private Expression handle(CustomDataPermission classAnnotation, Expression where, String methodName) {
//...jsqlparser处理sql,可参照前一篇
}
3.使用
mapper
@Mapper
@CustomDataPermission(
billType = BillTypeEnum.APPLY_BILL)
public interface ApplyHMapper extends MyBaseMapper<ApplyHPo> {
@CustomDataPermission(
billType = BillTypeEnum.APPLY_BILL,
operation = OperationTypeEnum.SELECT)
Page<ApplyHPo> selectApplyHs(IPage<ApplyHPo> page, @Param(Constants.WRAPPER) QueryWrapper<ApplyHPo> queryWrapper);
}
service
@Service
public class ApplyHServiceImpl implements ApplyHService {
@Autowired
private ApplyHMapper applyHMapper;
public void update(ApplyH applyH) {
// 先校验单据是否存在
ApplyH oldApplyH = applyHMapper.selectById(applyH.getId());
if(Objects.isNull(oldApplyH)) {
throw new ResourceNotFoundException("申请单【" + applyH.getBillNo() + "】不存在");
}
// 再校验是否存在修改权限
oldApplyH = applyHMapper.selectByIdForEdit(applyH.getId());
if(Objects.isNull(oldApplyH)) {
throw new NoAccessException("没有修改申请单【" + applyH.getBillNo() + "】的权限");
}
// ...其他校验
// ResourceNotFoundException和BizException为我们自定义的异常类,有全局异常进行处理
}
}
4.新增权限控制
删改查的权限控制实际上就是在sql后面拼接条件,
比如要控制门店字段orgcode,原本的sql为 select * from t_org where status = true
会处理成select * from t_org where status = true and orgcode in (…)
但是insert语句是没办法添加where条件的,就不能使用这个方法处理了,我们的思路是使用aop拦截自定义的注解,取到参数进行校验,这要求参数必须规范,大家有什么其他思路,欢迎交流
总结
对上一篇进行了加强,简化了开发,优化了权限控制逻辑