注解实现MyBaitis数据域查询

前言

因为系统本身设计问题,没有做横向数据越权的判断,导致目前的系统会出现横向数据越权,于是就把这一块写到注解中,并把注解标注在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必须去继承MybatisPlusBaseMapper才可以,当时在做这一块的时候也查找了很多资料,想不用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;
   }
}
复制代码

新建一个类去继承MybatisPlusAbstractMethod类,并实现其中的方法,并把当前类添加到

/**
 * 
 * @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;
}
复制代码

以上这个功能就写好了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享