• MyBatis-Plus 使用拦截器实现数据权限控制


    平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。列表实现方案有两种,一是在开发初期就做好判断赛选,但如果这个需求是中途加的,或不希望每个接口都加一遍,就可以方案二加拦截器的方式。在mybatis执行sql前修改语句,限定where范围。

    当然拦截器生效后是全局性的,如何保证只对需要的接口进行拦截和转化,就可以应用注解进行识别

    因此具体需要哪些步骤就明确了

    • 创建注解类

    • 创建拦截器实现InnerInterceptor接口,重写查询方法

    • 创建处理类,获取数据权限 SQL 片段,设置where

    • 将拦截器加到MyBatis-Plus插件中

    上代码(基础版)

    自定义注解

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Target({ElementType.METHOD, ElementType.TYPE})
    6. @Retention(RetentionPolicy.RUNTIME)
    7. public @interface UserDataPermission {
    8. }

    拦截器

    1. import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
    2. import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
    3. import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
    4. import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
    5. import lombok.*;
    6. import net.sf.jsqlparser.expression.Expression;
    7. import net.sf.jsqlparser.statement.select.PlainSelect;
    8. import net.sf.jsqlparser.statement.select.Select;
    9. import net.sf.jsqlparser.statement.select.SelectBody;
    10. import net.sf.jsqlparser.statement.select.SetOperationList;
    11. import org.apache.ibatis.executor.Executor;
    12. import org.apache.ibatis.mapping.BoundSql;
    13. import org.apache.ibatis.mapping.MappedStatement;
    14. import org.apache.ibatis.session.ResultHandler;
    15. import org.apache.ibatis.session.RowBounds;
    16. import java.sql.SQLException;
    17. import java.util.List;
    18. @Data
    19. @NoArgsConstructor
    20. @AllArgsConstructor
    21. @ToString(callSuper = true)
    22. @EqualsAndHashCode(callSuper = true)
    23. public class MyDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
    24.     /**
    25.      * 数据权限处理器
    26.      */
    27.     private MyDataPermissionHandler dataPermissionHandler;
    28.     @Override
    29.     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    30.         if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
    31.             return;
    32.         }
    33.         PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    34.         mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
    35.     }
    36.     @Override
    37.     protected void processSelect(Select select, int indexString sql, Object obj) {
    38.         SelectBody selectBody = select.getSelectBody();
    39.         if (selectBody instanceof PlainSelect) {
    40.             this.setWhere((PlainSelect) selectBody, (String) obj);
    41.         } else if (selectBody instanceof SetOperationList) {
    42.             SetOperationList setOperationList = (SetOperationList) selectBody;
    43.             List<SelectBody> selectBodyList = setOperationList.getSelects();
    44.             selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
    45.         }
    46.     }
    47.     /**
    48.      * 设置 where 条件
    49.      *
    50.      * @param plainSelect  查询对象
    51.      * @param whereSegment 查询条件片段
    52.      */
    53.     private void setWhere(PlainSelect plainSelect, String whereSegment) {
    54.         Expression sqlSegment = this.dataPermissionHandler.getSqlSegment(plainSelect, whereSegment);
    55.         if (null != sqlSegment) {
    56.             plainSelect.setWhere(sqlSegment);
    57.         }
    58.     }
    59. }

    拦截器处理器

    基础只涉及 = 表达式,要查询集合范围 in 看进阶版用例

    1. import cn.hutool.core.collection.CollectionUtil;
    2. import lombok.SneakyThrows;
    3. import lombok.extern.slf4j.Slf4j;
    4. import net.sf.jsqlparser.expression.Alias;
    5. import net.sf.jsqlparser.expression.Expression;
    6. import net.sf.jsqlparser.expression.HexValue;
    7. import net.sf.jsqlparser.expression.StringValue;
    8. import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
    9. import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
    10. import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
    11. import net.sf.jsqlparser.expression.operators.relational.InExpression;
    12. import net.sf.jsqlparser.expression.operators.relational.ItemsList;
    13. import net.sf.jsqlparser.schema.Column;
    14. import net.sf.jsqlparser.schema.Table;
    15. import net.sf.jsqlparser.statement.select.PlainSelect;
    16. import java.lang.reflect.Method;
    17. import java.util.List;
    18. import java.util.Objects;
    19. import java.util.Set;
    20. import java.util.stream.Collectors;
    21. @Slf4j
    22. public class MyDataPermissionHandler {
    23.     /**
    24.      * 获取数据权限 SQL 片段
    25.      *
    26.      * @param plainSelect  查询对象
    27.      * @param whereSegment 查询条件片段
    28.      * @return JSqlParser 条件表达式
    29.      */
    30.     @SneakyThrows(Exception.class)
    31.     public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
    32.         // 待执行 SQL Where 条件表达式
    33.         Expression where = plainSelect.getWhere();
    34.         if (where == null) {
    35.             where = new HexValue(" 1 = 1 ");
    36.         }
    37.         log.info("开始进行权限过滤,where: {},mappedStatementId: {}", where, whereSegment);
    38.         //获取mapper名称
    39.         String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
    40.         //获取方法名
    41.         String methodName = whereSegment.substring(whereSegment.lastIndexOf("."+ 1);
    42.         Table fromItem = (Table) plainSelect.getFromItem();
    43.         // 有别名用别名,无别名用表名,防止字段冲突报错
    44.         Alias fromItemAlias = fromItem.getAlias();
    45.         String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();
    46.         //获取当前mapper 的方法
    47.         Method[] methods = Class.forName(className).getMethods();
    48.         //遍历判断mapper 的所以方法,判断方法上是否有 UserDataPermission
    49.         for (Method m : methods) {
    50.             if (Objects.equals(m.getName(), methodName)) {
    51.                 UserDataPermission annotation = m.getAnnotation(UserDataPermission.class);
    52.                 if (annotation == null) {
    53.                     return where;
    54.                 }
    55.                 // 1、当前用户Code
    56.              User user = SecurityUtils.getUser();
    57.                 // 查看自己的数据
    58.                  //  = 表达式
    59.                  EqualsTo usesEqualsTo = new EqualsTo();
    60.                  usesEqualsTo.setLeftExpression(new Column(mainTableName + ".creator_code"));
    61.                  usesEqualsTo.setRightExpression(new StringValue(user.getUserCode()));
    62.                  return new AndExpression(where, usesEqualsTo);
    63.             }
    64.         }
    65.         //说明无权查看,
    66.         where = new HexValue(" 1 = 2 ");
    67.         return where;
    68.     }
    69. }

    将拦截器加到MyBatis-Plus插件中

    如果你之前项目配插件 ,直接用下面方式就行

    1. @Bean
    2. public MybatisPlusInterceptor mybatisPlusInterceptor() {
    3.     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    4.     // 添加数据权限插件
    5.     MyDataPermissionInterceptor dataPermissionInterceptor = new MyDataPermissionInterceptor();
    6.     // 添加自定义的数据权限处理器
    7.     dataPermissionInterceptor.setDataPermissionHandler(new MyDataPermissionHandler());
    8.     interceptor.addInnerInterceptor(dataPermissionInterceptor);
    9.     interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    10.     return interceptor;
    11. }

    但如果你项目之前是依赖包依赖,或有公司内部统一拦截设置好,也可以往MybatisPlusInterceptor进行插入,避免影响原有项目配置

    1. @Bean
    2. public MyDataPermissionInterceptor myInterceptor(MybatisPlusInterceptor mybatisPlusInterceptor) {
    3.     MyDataPermissionInterceptor sql = new MyDataPermissionInterceptor();
    4.     sql.setDataPermissionHandler(new MyDataPermissionHandler());
    5.     List<InnerInterceptor> list = new ArrayList<>();
    6.     // 添加数据权限插件
    7.     list.add(sql);
    8.     // 分页插件
    9.     mybatisPlusInterceptor.setInterceptors(list);
    10.     list.add(new PaginationInnerInterceptor(DbType.MYSQL));
    11.     return sql;
    12. }

    以上就是简单版的是拦截器修改语句使用

    使用方式

    在mapper层添加注解即可

    1. @UserDataPermission
    2. List selectAllCustomerPage(IPage page, @Param("customerName")String customerName);

    进阶版

    基础班只是能用,业务功能没有特别约束,先保证能跑起来

    进阶版 解决两个问题:

    • 加了角色,用角色决定范围

    • 解决不是mapper层自定义sql查询问题。

    两个是完全独立的问题 ,可根据情况分开解决

    解决不是mapper层自定义sql查询问题。

    例如我们名称简单的sql语句 直接在Service层用mybatisPluse自带的方法

    1. xxxxService.list(Wrapper<T> queryWrapper)
    2. xxxxService.page(new Page<>(),Wrapper<T> queryWrapper)

    以上这种我应该把注解加哪里呢

    因为service层,本质上还是调mapper层, 所以还是在mapper层做文章,原来的mapper实现了extends BaseMapper 接口,所以能够查询,我们要做的就是在 mapper层中间套一个中间接口,来方便我们加注解

    xxxxxMapper ——》DataPermissionMapper(中间) ——》BaseMapper

    根据自身需要,在重写的接口方法上加注解即可,这样就影响原先的代码

    图片

    1. import com.baomidou.mybatisplus.core.conditions.Wrapper;
    2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    3. import com.baomidou.mybatisplus.core.metadata.IPage;
    4. import com.baomidou.mybatisplus.core.toolkit.Constants;
    5. import org.apache.ibatis.annotations.Param;
    6. import java.io.Serializable;
    7. import java.util.Collection;
    8. import java.util.List;
    9. import java.util.Map;
    10. public interface DataPermissionMapper<T> extends BaseMapper<T> {
    11.     /**
    12.      * 根据 ID 查询
    13.      *
    14.      * @param id 主键ID
    15.      */
    16.     @Override
    17.     @UserDataPermission
    18.     T selectById(Serializable id);
    19.     /**
    20.      * 查询(根据ID 批量查询)
    21.      *
    22.      * @param idList 主键ID列表(不能为 null 以及 empty)
    23.      */
    24.     @Override
    25.     @UserDataPermission
    26.     List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
    27.     /**
    28.      * 查询(根据 columnMap 条件)
    29.      *
    30.      * @param columnMap 表字段 map 对象
    31.      */
    32.     @Override
    33.     @UserDataPermission
    34.     List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<StringObject> columnMap);
    35.     /**
    36.      * 根据 entity 条件,查询一条记录
    37.      *
    38.      * @param queryWrapper 实体对象封装操作类(可以为 null
    39.      */
    40.     @Override
    41.     @UserDataPermission
    42.     T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
    43.     /**
    44.      * 根据 Wrapper 条件,查询总记录数
    45.      *
    46.      * @param queryWrapper 实体对象封装操作类(可以为 null
    47.      */
    48.     @Override
    49.     @UserDataPermission
    50.     Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
    51.     /**
    52.      * 根据 entity 条件,查询全部记录
    53.      *
    54.      * @param queryWrapper 实体对象封装操作类(可以为 null
    55.      */
    56.     @Override
    57.     @UserDataPermission
    58.     List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
    59.     /**
    60.      * 根据 Wrapper 条件,查询全部记录
    61.      *
    62.      * @param queryWrapper 实体对象封装操作类(可以为 null
    63.      */
    64.     @Override
    65.     @UserDataPermission
    66.     List<Map<StringObject>> selectMaps(@Param(Constants.WRAPPER) Wrapper queryWrapper);
    67.     /**
    68.      * 根据 Wrapper 条件,查询全部记录
    69.      * <p>注意: 只返回第一个字段的值</p>
    70.      *
    71.      * @param queryWrapper 实体对象封装操作类(可以为 null
    72.      */
    73.     @Override
    74.     @UserDataPermission
    75.     List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
    76.     /**
    77.      * 根据 entity 条件,查询全部记录(并翻页)
    78.      *
    79.      * @param page         分页查询条件(可以为 RowBounds.DEFAULT
    80.      * @param queryWrapper 实体对象封装操作类(可以为 null
    81.      */
    82.     @Override
    83.     @UserDataPermission
    84.     <E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper queryWrapper);
    85.     /**
    86.      * 根据 Wrapper 条件,查询全部记录(并翻页)
    87.      *
    88.      * @param page         分页查询条件
    89.      * @param queryWrapper 实体对象封装操作类
    90.      */
    91.     @Override
    92.     @UserDataPermission
    93.     <E extends IPage<Map<StringObject>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper queryWrapper);
    94. }

    解决角色控制查询范围

    引入角色,我们先假设有三种角色,按照常规的业务需求,一种是管理员查看全部、一种是部门管理查看本部门、一种是仅查看自己。

    有了以上假设,就可以设置枚举类编写业务逻辑, 对是业务逻辑,所以我们只需要更改”拦截器处理器类“

    • 建立范围枚举

    • 建立角色枚举以及范围关联关系

    • 重写拦截器处理方法

    范围枚举

    1. @AllArgsConstructor
    2. @Getter
    3. public enum DataScope {
    4.     // Scope 数据权限范围 : ALL(全部)、DEPT(部门)、MYSELF(自己)
    5.     ALL("ALL"),
    6.     DEPT("DEPT"),
    7.     MYSELF("MYSELF");
    8.     private String name;
    9. }

    角色枚举

    1. @AllArgsConstructor
    2. @Getter
    3. public enum DataPermission {
    4.     // 枚举类型根据范围从前往后排列,避免影响getScope
    5.     // Scope 数据权限范围 : ALL(全部)、DEPT(部门)、MYSELF(自己)
    6.     DATA_MANAGER("数据管理员""DATA_MANAGER",DataScope.ALL),
    7.     DATA_AUDITOR("数据审核员""DATA_AUDITOR",DataScope.DEPT),
    8.     DATA_OPERATOR("数据业务员""DATA_OPERATOR",DataScope.MYSELF);
    9.     private String name;
    10.     private String code;
    11.     private DataScope scope;
    12.     public static String getName(String code) {
    13.         for (DataPermission type : DataPermission.values()) {
    14.             if (type.getCode().equals(code)) {
    15.                 return type.getName();
    16.             }
    17.         }
    18.         return null;
    19.     }
    20.     public static String getCode(String name) {
    21.         for (DataPermission type : DataPermission.values()) {
    22.             if (type.getName().equals(name)) {
    23.                 return type.getCode();
    24.             }
    25.         }
    26.         return null;
    27.     }
    28.     public static DataScope getScope(Collection<String> code) {
    29.         for (DataPermission type : DataPermission.values()) {
    30.             for (String v : code) {
    31.                 if (type.getCode().equals(v)) {
    32.                     return type.getScope();
    33.                 }
    34.             }
    35.         }
    36.         return DataScope.MYSELF;
    37.     }
    38. }

    重写拦截器处理类 MyDataPermissionHandler

    1. import lombok.SneakyThrows;
    2. import lombok.extern.slf4j.Slf4j;
    3. import net.sf.jsqlparser.expression.Alias;
    4. import net.sf.jsqlparser.expression.Expression;
    5. import net.sf.jsqlparser.expression.HexValue;
    6. import net.sf.jsqlparser.expression.StringValue;
    7. import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
    8. import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
    9. import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
    10. import net.sf.jsqlparser.expression.operators.relational.InExpression;
    11. import net.sf.jsqlparser.expression.operators.relational.ItemsList;
    12. import net.sf.jsqlparser.schema.Column;
    13. import net.sf.jsqlparser.schema.Table;
    14. import net.sf.jsqlparser.statement.select.PlainSelect;
    15. import java.lang.reflect.Method;
    16. import java.util.List;
    17. import java.util.Objects;
    18. import java.util.Set;
    19. import java.util.stream.Collectors;
    20. @Slf4j
    21. public class MyDataPermissionHandler {
    22.     private RemoteRoleService remoteRoleService;
    23.     private RemoteUserService remoteUserService;
    24.     /**
    25.      * 获取数据权限 SQL 片段
    26.      *
    27.      * @param plainSelect  查询对象
    28.      * @param whereSegment 查询条件片段
    29.      * @return JSqlParser 条件表达式
    30.      */
    31.     @SneakyThrows(Exception.class)
    32.     public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
    33.         remoteRoleService = SpringUtil.getBean(RemoteRoleService.class);
    34.         remoteUserService = SpringUtil.getBean(RemoteUserService.class);
    35.         // 待执行 SQL Where 条件表达式
    36.         Expression where = plainSelect.getWhere();
    37.         if (where == null) {
    38.             where = new HexValue(" 1 = 1 ");
    39.         }
    40.         log.info("开始进行权限过滤,where: {},mappedStatementId: {}", where, whereSegment);
    41.         //获取mapper名称
    42.         String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
    43.         //获取方法名
    44.         String methodName = whereSegment.substring(whereSegment.lastIndexOf("."+ 1);
    45.         Table fromItem = (Table) plainSelect.getFromItem();
    46.         // 有别名用别名,无别名用表名,防止字段冲突报错
    47.         Alias fromItemAlias = fromItem.getAlias();
    48.         String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();
    49.         //获取当前mapper 的方法
    50.         Method[] methods = Class.forName(className).getMethods();
    51.         //遍历判断mapper 的所以方法,判断方法上是否有 UserDataPermission
    52.         for (Method m : methods) {
    53.             if (Objects.equals(m.getName(), methodName)) {
    54.                 UserDataPermission annotation = m.getAnnotation(UserDataPermission.class);
    55.                 if (annotation == null) {
    56.                     return where;
    57.                 }
    58.                 // 1、当前用户Code
    59.                 User user = SecurityUtils.getUser();
    60.                 // 2、当前角色即角色或角色类型(可能多种角色)
    61.                 Set<String> roleTypeSet = remoteRoleService.currentUserRoleType();
    62.                 
    63.                 DataScope scopeType = DataPermission.getScope(roleTypeSet);
    64.                 switch (scopeType) {
    65.                     // 查看全部
    66.                     case ALL:
    67.                         return where;
    68.                     case DEPT:
    69.                         // 查看本部门用户数据
    70.                         // 创建IN 表达式
    71.                         // 创建IN范围的元素集合
    72.                         List<String> deptUserList = remoteUserService.listUserCodesByDeptCodes(user.getDeptCode());
    73.                         // 把集合转变为JSQLParser需要的元素列表
    74.                         ItemsList deptList = new ExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList()));
    75.                         InExpression inExpressiondept = new InExpression(new Column(mainTableName + ".creator_code"), deptList);
    76.                         return new AndExpression(where, inExpressiondept);
    77.                     case MYSELF:
    78.                         // 查看自己的数据
    79.                         //  = 表达式
    80.                         EqualsTo usesEqualsTo = new EqualsTo();
    81.                         usesEqualsTo.setLeftExpression(new Column(mainTableName + ".creator_code"));
    82.                         usesEqualsTo.setRightExpression(new StringValue(user.getUserCode()));
    83.                         return new AndExpression(where, usesEqualsTo);
    84.                     default:
    85.                         break;
    86.                 }
    87.             }
    88.         }
    89.         //说明无权查看,
    90.         where = new HexValue(" 1 = 2 ");
    91.         return where;
    92.     }
    93. }

    以上就是全篇知识点, 需要注意的点可能有:

    • 记得把拦截器加到MyBatis-Plus的插件中,确保生效

    • 要有一个业务赛选标识字段, 这里用的创建人 creator_code, 也可以用dept_code 等等

  • 相关阅读:
    taro超过3行隐藏显示展开功能
    [NOI2011] 阿狸的打字机
    数据库sql中判断时间冲突
    X86(32位)汇编指令与机器码转换原理
    计算机毕业设计Java慧学IT精品课程网站(源码+系统+mysql数据库+lw文档)
    特征工程学习笔记
    垃圾堆—>初赛错题集(待更)
    【物理应用】基于Matlab模拟极化雷达回波
    【初识Linux】:背景介绍以及环境搭建
    python3:len()返回对象的长度(元素数量) 2023-11-17
  • 原文地址:https://blog.csdn.net/yuechuzhixing/article/details/133016109