前言
因为系统本身设计问题,没有做横向数据越权的判断,导致目前的系统会出现横向数据越权,于是就把这一块写到注解中,并把注解标注在mapper层。
环境
环境 | 版本号 |
---|---|
springboot | 2.4.1 |
MybaitisPlus | 3.4.1 |
jsqlparser(MP中自动集成) | 4.0 |
思考
-
首先要想在
mybaitis
中实现此功能,肯定要用到mybaitis
的拦截器,犹豫我们本身集成了mybatisplus
,而mybatisplus
本身也集成了mybatis
并暴漏了一个拦截器接口InnerInterceptor
,我们只要实现InnerInterceptor
并重写其中的beforeQuery
方法就可以了。 -
其次要想在拦截器中实现根据注解来拦截
sql
语句的方法,时需要用到反射的,这样时很耗费性能,那么我们为什么不能在启动程序时对有注解的sql
语句做一个全局的缓存呢,key就是类名+方法名
具体实现
- 设计注解
/**
* @author lin
* @date 2021/8/5
*/
@Documented
@Target({METHOD})
@Retention(RUNTIME)
public @interface DataScope {
/**
* sql列别名
*
* @return {@link String }
* @author lin
*/
String columnAlias();
/**
* 实体类中deptId的名字
*
* @return {@link String }
* @author lin
*/
String deptIdName() default "";
/**
* 判断其是否需要查询下级
*
* @return boolean
* @author lin
*/
boolean subordinate() default true;
}
复制代码
deptIdName
这个方法在使用时,是配合spel表达式来使用的,目的是判断前端是否把deptId
作为查找条件, 入参的实体类上的 deptId
字段没有值。
- 在
SpringBoot
启动时,对有注解的mapper层方法做一个全局缓存。
在做这一层的时候是有一个约束的,就是你当前的Mapper
必须去继承MybatisPlus
的BaseMapper
才可以,当时在做这一块的时候也查找了很多资料,想不用MybatisPlus
的东西,但是发现并没有什么好的实现,也希望后面有小伙伴可以给提供另一种解题思路。
/**
*
* @author lin
* @date 2021/8/6
* @since 2021/8/6 16:49
*/
@Data
public class DataScopeDTO {
/**
* 数据域注解
*/
private DataScope dataScope;
/**
* 参数名称
*/
private String[] parameterName;
}
复制代码
做一个DTO类当成缓存的Value,类中的属性一个是DataScope
注解,一个是方法参数的参数名,这样的目的是方便Spel解析
/**
*
*
* @author lin
* @date 2021/8/5
* @since 2021/8/5 10:49
*/
public class CacheTableInfo extends AbstractMethod {
private static final long serialVersionUID = 9049345712746536158L;
public static Map<String, DataScopeDTO> dataScopeMap = new HashMap<>();
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
Method[] methods = mapperClass.getMethods();
ArrayList<String> stringList = new ArrayList<>();
for (Method method : methods) {
if (method.isAnnotationPresent(DataScope.class)) {
DataScopeDTO dataScopeDTO = new DataScopeDTO();
dataScopeDTO.setDataScope(method.getAnnotation(DataScope.class));
String idName = mapperClass.getName()+"."+method.getName();
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
stringList.add(parameter.getName());
}
String[] parameterNames = stringList.toArray(new String[0]);
dataScopeDTO.setParameterName(parameterNames);
dataScopeMap.put(idName,dataScopeDTO);
}
}
return null;
}
}
复制代码
新建一个类去继承MybatisPlus
的AbstractMethod
类,并实现其中的方法,并把当前类添加到
/**
*
* @date: 2021/4/22 16:47
*
*/
public class FnySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new CacheTableInfo());
return methodList;
}
}
复制代码
这样在程序启动的时候就回去遍历所有继承BaseMapper
的mapper层的类,并通过反射可以获取当每个类的方法上有没有注解,如果有注解就可以做一个缓存;
- 实现
MyBatisPlus的InnerInterceptor
接口并实现beforeQuery
方法
/**
*
* @author lin
* @date 2021/8/4
* @since 2021/8/4 10:25
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class DataScopeInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
private String columnName;
private DataScope dataScope;
private String deptId;
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
DataScopeDTO dataScopeDTO = CacheTableInfo.dataScopeMap.get(ms.getId());
if (dataScopeDTO == null) {
return;
}
this.dataScope = dataScopeDTO.getDataScope();
this.setColumnName(this.dataScope.columnAlias());
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
Object parameterObject = mpBs.parameterObject();
String[] parameterNames = dataScopeDTO.getParameterName();
if(Objects.isNull(parameterNames) ) {
return;
}
//是否有关于deptId的查询条件 如果有就进行解析,如果没有就为空值
if (StrUtil.isNotEmpty(this.dataScope.deptIdName())) {
this.deptId = parserDeptId(parameterNames, parameterObject, dataScope.deptIdName());
}else {
this.deptId = "";
}
//当前用户为admin 且deptId为空查询全部
if (UserUtil.isAdmin() && StrUtil.isEmpty(deptId)) {
return;
}
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
//有deptId查询条件
if (StrUtil.isNotEmpty(this.deptId)) {
this.getExpression(this.deptId,plainSelect);
}else {
//不是admin 且deptId 查询条件为空或者没有deptId的查询条件
this.getExpression(UserUtil.getDeptId(),plainSelect);
}
}
/**
* 解析deptId
*
* @param parameterNames 参数名称
* @param parameterObject 参数对象
* @param scopeDeptId 范围部门id
* @return @return {@link String }
* @author lin
*/
private String parserDeptId(String[] parameterNames, Object parameterObject, String scopeDeptId) {
StandardEvaluationContext context = new StandardEvaluationContext();
//获取参数名称对应的参数值
if (parameterObject instanceof Map) {
Map<String, ?> parameterMap = (Map<String, ?>) parameterObject;
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], parameterMap.get(parameterNames[i]));
}
} else {
context.setVariable(parameterNames[0], parameterObject);
}
org.springframework.expression.Expression expression = PARSER.parseExpression(scopeDeptId);
return expression.getValue(context, String.class);
}
private void getExpression(String deptId,PlainSelect plainSelect) {
Expression where = plainSelect.getWhere();
SysChildDeptService service = SpringUtil.getBean(SysChildDeptService.class);
//判断不需要查询下级
if (this.dataScope.subordinate()) {
List<SysDeptDO> deptList = service.getChildDeptList(deptId);
ItemsList itemsList = new ExpressionList(deptList.stream()
.map(dept -> new StringValue(dept.getDeptId()))
.collect(Collectors.toList()));
//创建insql
InExpression inExpression = new InExpression(new Column(this.getColumnName()), itemsList);
//拼接sql
AndExpression andExpression = new AndExpression(where, inExpression);
plainSelect.setWhere(andExpression);
}else {
EqualsTo equalsTo = new EqualsTo(new Column(this.getColumnName()),new StringValue(deptId));
AndExpression andExpression = new AndExpression(where, equalsTo);
plainSelect.setWhere(andExpression);
}
}
}
复制代码
其中继承的JsqlParserSupport
类,是jsqlParse包下的一个类,有兴趣的小伙伴可以去百度一下这个jar包的使用方法,我也是照葫芦画瓢写上去的
- 小问题
这样基本就完成了一个数据权限的拦截器,然后只需要把这个拦截器添加到MybatisPlus
的拦截器配置中就可以了
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new DataScopeInnerInterceptor());
return interceptor;
}
复制代码
但是这样会有一个小问题就是在同时配置了分页的情况下会先分页,在拼接修改的sql,就会出现数据不正确的问题
可以把两个拦截器的位置进行一下互换就可以了。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataScopeInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
复制代码
以上这个功能就写好了。