03、Mybatis-Plus入门 - 数据权限插件DataPermissionInterceptor

前言

在plus3.4.2版本中,阅读源码时发现了一个新插件DataPermissionInterceptor,但是在官网没有相关说明,从注释数据权限处理器来看,实现的功能是数据权限。

/**
 * 数据权限处理器
 *
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @since 3.4.1 +
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@SuppressWarnings({
   
     "rawtypes"})
public class DataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
   
     
    private DataPermissionHandler dataPermissionHandler;

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
   
     
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) return;
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
   
     
        PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), (String) obj);
        if (null != sqlSegment) {
   
     
            plainSelect.setWhere(sqlSegment);
        }
    }
}

源码分析

1、 继承抽象类JsqlParserSupport并重写processSelect方法JSqlParser是一个SQL语句解析器,它将SQL转换为Java类的可遍历层次结构plus中也引入了JSqlParser包,processSelect可以对Select语句进行处理;
2、 实现InnerInterceptor接口并重写beforeQuery方法InnerInterceptor是plus的插件接口,beforeQuery可以对查询语句执行前进行处理;
3、 DataPermissionHandler作为数据权限处理器,是一个接口,提供getSqlSegment方法添加数据权限SQL片段;
4、 由上可知,我们只需要实现DataPermissionHandler接口,并按照业务规则处理SQL,就可以实现数据权限的功能;

/**
 * 数据权限处理器
 *
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @since 3.4.1 +
 */
public interface DataPermissionHandler {
   
     

    /**
     *
     * @param where             待执行 SQL Where 条件表达式
     * @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
     * @return JSqlParser 条件表达式
     */
    Expression getSqlSegment(Expression where, String mappedStatementId);
}

使用案例

实现同一个查询接口,本门经理只能查看当前部门的所有数据,老板则能查看所有部门数据类似的功能,员工则只能查看自己数据的数据权限功能。

1、 创建部门枚举类;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum  DeptEnum {
   
     

    BOOS(1,"老总"),

    MANAGER_01(2,"01部门经理"),

    MANAGER_06(6,"06部门经理");
    
    int type;
    String desc;
}

1、 表及实体类添加部门编号;
  2、 编写MyDataPermissionHandler类,继承DataPermissionHandler,编写数据权限处理逻辑,对SQL进行拦截处理;

@Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler {
   
     

    /**
     * @param where             原SQL Where 条件表达式
     * @param mappedStatementId Mapper接口方法ID
     * @return
     */
    @SneakyThrows
    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
   
     
        log.info("=========================== start MyDataPermissionHandler");
        // 1. 模拟获取登录用户,从用户信息中获取部门ID
        Random random = new Random();
        int userDeptId = random.nextInt(9) + 1; // 随机部门ID 1-10 随机数
        Expression expression = null;
        log.info("=============== userDeptId:{}", userDeptId);
        if (userDeptId == DeptEnum.BOOS.getType()) {
   
     
            // 2.userDeptId为1,说明是老总,可查看所有数据无需处理
            return where;

        } else if (userDeptId == DeptEnum.MANAGER_02.getType()) {
   
     
            // 3. userDeptId为2,说明是02部门经理,可查看02部门及下属部门所有数据
            // 创建IN 表达式
            Set<String> deptIds = Sets.newLinkedHashSet(); // 创建IN范围的元素集合
            deptIds.add("2");
            deptIds.add("3");
            deptIds.add("4");
            deptIds.add("5");
            ItemsList itemsList = new ExpressionList(deptIds.stream().map(StringValue::new).collect(Collectors.toList())); // 把集合转变为JSQLParser需要的元素列表
            InExpression inExpression = new InExpression(new Column("order_tbl.dept_id"), itemsList); //  order_tbl.dept_id IN ('2', '3', '4', '5')
            return new AndExpression(where, inExpression);
        } else if (userDeptId == DeptEnum.MANAGER_06.getType()) {
   
     
            // 4. userDeptId为6,说明是06部门经理,可查看06部门及下属部门所有数据
            // 创建IN 表达式
            Set<String> deptIds = Sets.newLinkedHashSet(); // 创建IN范围的元素集合
            deptIds.add("6");
            deptIds.add("7");
            deptIds.add("8");
            deptIds.add("9");
            ItemsList itemsList = new ExpressionList(deptIds.stream().map(StringValue::new).collect(Collectors.toList())); // 把集合转变为JSQLParser需要的元素列表
            InExpression inExpression = new InExpression(new Column("order_tbl.dept_id"), itemsList);
            return new AndExpression(where, inExpression);
        } else {
   
     
            // 5. userDeptId为其他时,表示为员工级别没有下属机构,只能查看当前部门的数据
            //  = 表达式
            EqualsTo equalsTo = new EqualsTo(); // order_tbl.dept_id = userDeptId
            equalsTo.setLeftExpression(new Column("order_tbl.dept_id"));
            equalsTo.setRightExpression(new LongValue(userDeptId));
            // 创建 AND 表达式 拼接Where 和 = 表达式
            return new AndExpression(where, equalsTo); // WHERE user_id = 2 AND order_tbl.dept_id = 3
        }
    }
}

1、 配置类添加数据权限插件;

        // 添加数据权限插件
        DataPermissionInterceptor dataPermissionInterceptor=new DataPermissionInterceptor();
        MyDataPermissionHandler myDataPermissionHandler=new MyDataPermissionHandler();
        // 添加自定义的数据权限处理器
        dataPermissionInterceptor.setDataPermissionHandler(myDataPermissionHandler);
        interceptor.addInnerInterceptor(dataPermissionInterceptor);

案例测试

1、 当部门ID为1时,查询出来所有数据;
  2、 当部门ID为最下层机构时,只能查询到当前部门的数据;
  3、 当为部门经理时,查到看当前及下属所有机构的数据;
  4、 综上,通过简单的案例测试,能完成数据权限部分功能,可根据此深入,比如更深层次的按照用户、角色等绑定数据权限;

忽略拦截

plus提供了 @InterceptorIgnore方法对插件进行是否拦截配置,官网:该注解作用于 xxMapper.java 方法之上 各属性代表对应的插件 各属性不给值则默认为 false 设置为 true。既然此注解只能作用于Mapper,看来需要自己写一个注解,做用于service层,实现接口级别的拦截配置。。。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
   
     ElementType.TYPE, ElementType.METHOD})
public @interface InterceptorIgnore {
   
     

    /**
     * 行级租户 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor}
     */
    String tenantLine() default "";

    /**
     * 动态表名 {@link com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor}
     */
    String dynamicTableName() default "";

    /**
     * 攻击 SQL 阻断解析器,防止全表更新与删除 {@link com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor}
     */
    String blockAttack() default "";

    /**
     * 垃圾SQL拦截 {@link com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor}
     */
    String illegalSql() default "";

    /**
     * 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
     * <p>
     * 默认关闭,需要注解打开
     */
    String dataPermission() default "1";

    /**
     * 分表 {@link com.baomidou.mybatisplus.extension.plugins.inner.ShardingInnerInterceptor}
     */
    String sharding() default "";

    /**
     * 其他的
     * <p>
     * 格式应该为:  "key"+"@"+可选项[false,true,1,0,on,off]
     * 例如: "xxx@1" 或 "xxx@true" 或 "xxx@on"
     * <p>
     * 如果配置了该属性的注解是注解在 Mapper 上的,则如果该 Mapper 的一部分 Method 需要取反则需要在 Method 上注解并配置此属性为反值
     * 例如: "xxx@1" 在 Mapper 上, 则 Method 上需要 "xxx@0"
     */
    String[] others() default {
   
     };
}