MybatisPlus实现数据权限

1. 引言

在日常的项目开发过程中,数据权限是保障数据安全的重要手段。MybatisPlus 作为 Mybatis 的增强工具,在为开发者提供便捷的数据库操作的同时,也应支持灵活的数据权限控制。
下面我们来看一下如何借助 MybatisPlus 实现自定义注解的数据权限。

2. MybatisPlus 简介

MybatisPlus 是一款 Mybatis 的增强工具,它为 Mybatis 提供了丰富的查询接口,大大简化了数据库操作。MybatisPlus 具备代码生成器,支持一键生成 Entity、Mapper、Mapper XML、Service、Controller 等文件,有效提高开发效率。此外,MybatisPlus 还提供了分页插件、性能分析插件等,能够帮助开发者更好地优化数据库操作。

3.数据权限拦截器的原理

数据权限拦截器主要是在 MybatisPlus 的数据库操作过程中,额外加入权限判断的逻辑。其核心原理是通过自定义拦截器实现对 SQL 执行前的拦截,然后根据用户权限信息判断是否允许执行该 SQL。具体来说,数据权限拦截器需要实现 MybatisPlus 的拦截器接口,重写 intercept 方法,在其中添加权限判断逻辑。

4. 实现数据权限拦截器

要实现数据权限拦截器,首先需要在项目中引入 MybatisPlus 相关依赖,然后创建自定义拦截器类,实现 MybatisPlus 拦截器接口,并重写 intercept 方法。在 intercept 方法中,可以根据用户权限信息判断是否允许执行当前 SQL,如果禁止执行,则抛出异常或返回错误结果。

5. 代码案例

5.1 配置自定义拦截器

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataPermissionInterceptorConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 添加分页拦截器
        interceptor.addInnerInterceptor(dataPermissionInterceptor()); // 添加数据权限拦截器
        return interceptor;
    }

    /**
     * 数据权限插件
     */
    @Bean
    public DataPermissionInterceptor dataPermissionInterceptor() {
        return new DataPermissionInterceptor();
    }
}

5.2 自定义数据权限注解

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

    /**
     * 表别名-生成的数据权限 where 带表别名
     *
     * @return
     */
    String tableAlias() default "";

    /**
     * 是否驼峰转下划线
     *
     * @return
     */
    boolean ifHump() default false;

    /**
     * 自定义数据字段
     */
    DataColumn[] value();

    /**
     * 字段关系-每个数据权限方案内部多字段的关系
     */
    String colRelation() default SqlRelationConstants.AND;

    /**
     * 方案关系-支持多个数据权限方案,多数据权限方案的关系
     */
    String schRelation() default SqlRelationConstants.AND;
    
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {

    /**
     * 字段key
     */
    String key();

    /**
     * 字段名称
     */
    String[] columns();
}

5.3 自定义拦截器

  • 继承JsqlParserSupport类并重写processSelect方法。它是一个SQL语句解析器。processSelect可以对select语句进行处理。
  • 实现InnerInterceptor接口,并重写beforeQuery方法。InnerInterceptor是plus的插件接口,beforeQuery可以对查询语句执行前进行处理。
@Slf4j
public class DataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {

    @Resource
    private DataPermissionHandler dataPermissionHandler;

    @Override
    @SneakyThrows(Exception.class)
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // 检查是否无效 无数据权限注解
        if (dataPermissionHandler.isInvalid(ms.getId())) {
            return;
        }
        // 解析 sql 分配对应方法
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        String originalSql = mpBs.sql();
        String parserSql = originalSql;
        try {
            parserSql = parserSingle(originalSql, ms.getId());
        } catch (MybatisPlusException e) {
            log.error("sql解析异常:{}", e);
            throw new RuntimeException("数据权限 sql 解析异常:" + e.getMessage());
        }
        log.info("数据权限sql:{}", parserSql);
        mpBs.sql(parserSql);
    }

    @Override
    protected void processSelect(Select select, int index, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            this.setWhere((PlainSelect) selectBody, (String) obj);
        } else if (selectBody instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }

    /**
     * 设置 where 条件
     *
     * @param plainSelect       查询对象
     * @param mappedStatementId 执行方法id
     */
    protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }
}

5.4 数据权限处理器

处理器主要是构造数据权限 sql 语句并返回

@Slf4j
@Component
public class DataPermissionHandler {
	
	// 用于获取当前角色的数据权限
    @Resource
    private ManagementClientImpl managementClient;

    /**
     * 方法或类(名称) 与 注解的映射关系缓存
     */
    private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();

    /**
     * 无效注解方法缓存用于快速返回
     */
    private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();

    private static final String EMPTY_STR = " ";

    public Expression getSqlSegment(Expression where, String mappedStatementId) {
        log.info("开始进行权限过滤,where: {},mappedStatementId: {}", where, mappedStatementId);
        DataPermissionEntity dataPermissionEntity = findAnnotation(mappedStatementId);
        if (ObjectUtil.isNull(dataPermissionEntity)) {
            invalidCacheSet.add(mappedStatementId);
            return where;
        }

        String dataFilterSql = buildDataFilter(dataPermissionEntity);
        if (StringUtils.isBlank(dataFilterSql)) {
            return where;
        }
        try {
            log.info("sql:{}", dataFilterSql);
            Expression expression = CCJSqlParserUtil.parseCondExpression(dataFilterSql);
            // 数据权限使用单独的括号 防止与其他条件冲突
            Parenthesis parenthesis = new Parenthesis(expression);
            if (ObjectUtil.isNotNull(where)) {
                return new AndExpression(where, parenthesis);
            } else {
                return parenthesis;
            }
        } catch (JSQLParserException e) {
            e.printStackTrace();
            throw new RuntimeException("数据权限解析异常 => " + e.getMessage());
        }
    }

    /**
     * 获取注解参数
     *
     * @param mappedStatementId
     * @return
     */
    private DataPermissionEntity findAnnotation(String mappedStatementId) {
        StringBuilder sb = new StringBuilder(mappedStatementId);
        int index = sb.lastIndexOf(".");
        String clazzName = sb.substring(0, index);
        String methodName = sb.substring(index + 1, sb.length());
        Class<?> clazz = ClassUtil.loadClass(clazzName);
        List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
                .filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
        DataPermission dataPermission;
        // 获取方法注解
        for (Method method : methods) {
            dataPermission = dataPermissionCacheMap.get(mappedStatementId);
            if (ObjectUtil.isNotNull(dataPermission)) {
                return DataPermissionEntity.builder()
                        .tableAlias(dataPermission.tableAlias())
                        .ifHump(dataPermission.ifHump())
                        .columns(dataPermission.value())
                        .colRelation(dataPermission.colRelation())
                        .schRelation(dataPermission.schRelation())
                        .build();
            }
            dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
            if (ObjectUtil.isNotNull(dataPermission)) {
                dataPermissionCacheMap.put(mappedStatementId, dataPermission);
                return DataPermissionEntity.builder()
                        .tableAlias(dataPermission.tableAlias())
                        .ifHump(dataPermission.ifHump())
                        .columns(dataPermission.value())
                        .colRelation(dataPermission.colRelation())
                        .schRelation(dataPermission.schRelation())
                        .build();
            }
        }
        dataPermission = dataPermissionCacheMap.get(clazz.getName());
        if (ObjectUtil.isNotNull(dataPermission)) {
            return DataPermissionEntity.builder()
                    .tableAlias(dataPermission.tableAlias())
                    .ifHump(dataPermission.ifHump())
                    .columns(dataPermission.value())
                    .colRelation(dataPermission.colRelation())
                    .schRelation(dataPermission.schRelation())
                    .build();
        }
        // 获取类注解
        dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
        if (ObjectUtil.isNotNull(dataPermission)) {
            dataPermissionCacheMap.put(clazz.getName(), dataPermission);
            return DataPermissionEntity.builder()
                    .tableAlias(dataPermission.tableAlias())
                    .ifHump(dataPermission.ifHump())
                    .columns(dataPermission.value())
                    .colRelation(dataPermission.colRelation())
                    .schRelation(dataPermission.schRelation())
                    .build();
        }
        return null;
    }


    /**
     * 构造数据过滤sql
     *
     * @return
     */
    private String buildDataFilter(DataPermissionEntity dataPermissionEntity) {
        DataColumn[] valueArray = dataPermissionEntity.getColumns();
        if (valueArray == null || valueArray.length == 0) {
            log.error("未配置数据权限方案,数据权限无效!");
            return null;
        }

        String tableAlias = StringUtils.isNotBlank(dataPermissionEntity.getTableAlias())
                ? dataPermissionEntity.getTableAlias() + "." : "";
        boolean ifHump = dataPermissionEntity.isIfHump();
        String colRelation = dataPermissionEntity.getColRelation();
        String schRelation = dataPermissionEntity.getSchRelation();

        // sql语句
        StringBuffer sqlbf = new StringBuffer();
        for (int i = 0; i < valueArray.length; i++) {
            String schemeCode = valueArray[i].key();
            Map<String, Object> dataScopeMap = managementClient.getUserDataScope(schemeCode);
            if (CollectionUtil.isEmpty(dataScopeMap) || CollectionUtil.isEmpty(MapUtil.get(dataScopeMap, schemeCode, List.class))) {
                log.info("根据数据权限方案编码{}未查询到数据权限信息", schemeCode);
                // 方案的关系是and时,其中一个方案为空直接返回1=2;方案关系为or时,根据不为空的方法构建sql
                if (SqlRelationConstants.AND.equals(schRelation)) {
                    return "1 = 2";
                } else {
                    continue;
                }
            }
            log.info("map:{}", dataScopeMap);
            sqlbf.append(" (");
            List<String> columns = Arrays.asList(valueArray[i].columns());
            // 用户sql拼接
            for (int j = 0; j < columns.size(); j++) {
                String fieldCode = columns.get(j);
                if (ifHump) {
                    fieldCode = camelToUnderline(fieldCode);
                }
                sqlbf.append(tableAlias).append(fieldCode).append(EMPTY_STR).append("IN").append(EMPTY_STR)
                        .append(convertListIdToString(MapUtil.get(dataScopeMap, schemeCode, List.class)))
                        .append(" ");
                if (j != columns.size() - 1) {
                    sqlbf.append(colRelation).append(EMPTY_STR);
                }
            }
            // 若是多个方案,每个方案下多个字段,如:(方案a字段1 = '' and 方案a字段2 = '') or (方案b字段1 = '' or 方案b字段2 = '')
            // 保证每个权限方案的多个字段在一个括号内
            sqlbf.append(")").append(EMPTY_STR);

            if (i != valueArray.length - 1) {
                sqlbf.append(schRelation).append(EMPTY_STR);
            }
        }
        // sql规范
        String sqlStr = sqlbf.toString();
        if (sqlStr.endsWith(schRelation + EMPTY_STR)) {
            sqlStr = sqlStr.substring(0, sqlStr.lastIndexOf(schRelation));
        }

        return StringUtils.isNotBlank(sqlStr) ? sqlStr : " 1=2 ";
    }

    /**
     * 是否为无效方法 无数据权限
     */
    public boolean isInvalid(String mappedStatementId) {
        return invalidCacheSet.contains(mappedStatementId) || !this.hasDataPermissionAnotation(mappedStatementId);
    }

    /**
     * 判断是否存在数据权限注解
     *
     * @return
     */
    private boolean hasDataPermissionAnotation(String mappedStatementId) {

        if (ObjectUtil.isNotNull(dataPermissionCacheMap.get(mappedStatementId))) {
            return true;
        }
        StringBuilder sb = new StringBuilder(mappedStatementId);
        int index = sb.lastIndexOf(".");
        String clazzName = sb.substring(0, index);
        String methodName = sb.substring(index + 1, sb.length());
        Class<?> clazz = ClassUtil.loadClass(clazzName);

        // 获取类注解
        DataPermission dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
        if (ObjectUtil.isNull(dataPermission)) {
            List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
                    .filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
            if (CollectionUtil.isEmpty(methods) || methods.size() > 1) {
                invalidCacheSet.add(mappedStatementId);
                return false;
            }
            dataPermission = AnnotationUtil.getAnnotation(methods.get(0), DataPermission.class);
            if (ObjectUtil.isNotNull(dataPermission)) {
                dataPermissionCacheMap.put(mappedStatementId, dataPermission);
                return true;
            } else {
                invalidCacheSet.add(mappedStatementId);
                return false;
            }
        } else {
            dataPermissionCacheMap.put(clazz.getName(), dataPermission);
            return true;
        }
    }

    /**
     * 驼峰格式字符串转换为下划线格式字符串
     *
     * @param param
     * @return
     */
    public String camelToUnderline(String param) {
        if (param == null || "".equals(param.trim())) {
            return "";
        }
        int len = param.length();
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            char c = param.charAt(i);
            if (Character.isUpperCase(c)) {
                sb.append("_");
                sb.append(Character.toLowerCase(c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * 将List<String>转化为String
     */
    private String convertListIdToString(List<String> ids) {
        if (CollectionUtil.isEmpty(ids)) {
            return null;
        }
        StringBuffer idSBuffer = new StringBuffer();
        idSBuffer.append("(");
        ids.forEach(id -> {
            idSBuffer.append("'").append(id).append("',");
        });
        return idSBuffer.toString().substring(0, idSBuffer.lastIndexOf(",")).concat(") ");
    }
}

5.5 数据权限实体

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DataPermissionEntity {

    private String tableAlias;
    private boolean ifHump;
    private DataColumn[] columns;
    private String colRelation;
    private String schRelation;
}

5.6 构建 sql 的关联关系字段

public class SqlRelationConstants {

    public static final String AND = "and";
    public static final String OR = "or";
}

6. 使用

6.1 自定义 mapper 方法

@DataPermission(
            tableAlias = "tb",
            ifHump = true,
            value = {
                    @DataColumn(key = DataPermissionConstants.PARK, columns = "parkCode"),
                    @DataColumn(key = DataPermissionConstants.ISP, columns = "facilitatorCode"),
                    @DataColumn(key = DataPermissionConstants.MERCHANT, columns = "merchantCode")
            }
            colRelation = SqlRelationConstants.OR
    )
    List<EntityA> queryMethod(@Param("entityB") EntityB entityB);

构造的数据权限 sql 为:

(tb.park_code = xxx or tb.facilitator_code = yyy or tb.merchant_code = zzz)

6.2 原生MybatisPlus 方法

当 service 层或者 controller 层调用的是原生方法,此时在 mapper 类中是没有方法的,则我们需要重写此方法。如我们调用的是原生的selectPage方法。

public interface SysRoleMapper extends BaseMapper<SysRole> {
	@DataPermission(
            tableAlias = "tb",
            ifHump = true,
            value = {
                    @DataColumn(key = DataPermissionConstants.PARK, columns = "parkCode"),
                    @DataColumn(key = DataPermissionConstants.ISP, columns = "facilitatorCode"),
                    @DataColumn(key = DataPermissionConstants.MERCHANT, columns = "merchantCode")
            }
            colRelation = SqlRelationConstants.OR
    )
    @Override
    <P extends IPage<SysRole>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<SysRole> queryWrapper);
    
}

7. 总结

以上是 MybatisPlus 实现数据权限拦截器的方法,实现的方式比较复杂,因为功能比较多,可根据具体的业务场景进行删减代码。

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值