MyBatis-Plus数据权限插件

本文介绍了如何使用MyBatis-Plus实现数据权限插件,通过AOP注解和线程上下文传递参数,利用JSqlParser解析SQL动态添加过滤条件。插件集成包括DataScope注解、DataScopeInnerInterceptor拦截器和DataScopeHandler处理逻辑。示例展示了部门数据权限、部门及以下数据权限和仅本人数据权限的处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MyBatis-Plus数据权限插件

一、实现思路和注意事项

  • 实现思路

AOP注解和线程上下文变量传递参数到Mybatis-Plus SQL拦截器,使用JSqlParser AST SQL语法解析树操作Where动态添加过滤条件

  • 注意事项

@DataScope注解一般用于Service层或者DAO层(Mapper),用于Service层时,若方法中存在多个查询语句均会拼接数据权限过滤条件。

二、源码

1.pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

2.spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.datascope.autoconfigure.DataScopeAutoConfiguration

3.基础代码

  • DataScope
package com.example.datascope.annotation;

import com.example.datascope.enums.DataScopeType;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {

    /**
     * 范围权限类别
     */
    public DataScopeType type() default DataScopeType.DATA_SCOPE_SELF;

    /**
     * 部门表的别名
     */
    public String deptAlias() default "d";

    /**
     * 用户表的别名
     */
    public String userAlias() default "u";
}


  • EnableDataScopeAspect
package com.example.datascope.annotation;

import com.example.datascope.aspectj.DataScopeAspect;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * 启用数据权限AOP
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DataScopeAspect.class)
public @interface EnableDataScopeAspect {

}

  • DataScopeAspect
package com.example.datascope.aspectj;

import com.example.datascope.annotation.DataScope;
import com.example.datascope.context.DataScopeContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 数据过滤处理
 */
@Aspect
@Component
public class DataScopeAspect {

    @Before("@annotation(datascope)")
    public void doBefore(JoinPoint point, DataScope datascope) throws Throwable {
        resetContextHolders();
        initContextHolders(datascope);
    }

    protected void initContextHolders(DataScope dataScope) {
        DataScopeContextHolder.set(dataScope);
    }

    private void resetContextHolders() {
        DataScopeContextHolder.remove();
    }

}

  • DataScopeAutoConfiguration
package com.example.datascope.autoconfigure;

import com.example.datascope.annotation.EnableDataScopeAspect;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;

@EnableDataScopeAspect
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "datascope", value = "enable", havingValue = "true", matchIfMissing = true)
public class DataScopeAutoConfiguration {

}

  • DataScopeContextHolder
package com.example.datascope.context;

import com.example.datascope.annotation.DataScope;

/**
 * DataScope上下文对象
 */
public abstract class DataScopeContextHolder {

    private static final ThreadLocal<DataScope> context = new InheritableThreadLocal<>();

    public static void set(DataScope dataScope) {
        context.set(dataScope);
    }

    public static DataScope get() {
        return context.get();
    }

    public static void remove() {
        context.remove();
    }

}

  • DataScopeType
package com.example.datascope.enums;

import java.util.Objects;

public enum DataScopeType {
    /**
     * 全部数据权限
     */
    DATA_SCOPE_ALL("1"),
    /**
     * 自定数据权限
     */
    DATA_SCOPE_CUSTOM("2"),
    /**
     * 部门数据权限
     */
    DATA_SCOPE_DEPT("3"),
    /**
     * 部门及以下数据权限
     */
    DATA_SCOPE_DEPT_AND_CHILD("4"),
    /**
     * 仅本人数据权限
     */
    DATA_SCOPE_SELF("5");

    private String code;

    public String getCode() {
        return code;
    }

    DataScopeType(String code) {
        this.code = code;
    }

    public static DataScopeType of(String code) {

        Objects.requireNonNull(code, "数据范围权限类型不允许为空");

        for (DataScopeType dataScopeType : DataScopeType.values()) {
            if (dataScopeType.getCode().equals(code)) {
                return dataScopeType;
            }
        }

        throw new IllegalArgumentException(String.format("未识别的数据范围权限类型值[%s]", code));
    }


}

4.DataScopeInnerInterceptor

/*
 * Copyright (c) 2011-2022, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.datascope.plugins;

import com.example.datascope.enums.DataScopeType;
import com.example.datascope.annotation.DataScope;
import com.example.datascope.context.DataScopeContextHolder;
import com.example.datascope.handler.DataScopeHandler;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.List;

/**
 * Mybatis-Plus数据权限插件
 *
 * 
 * @see TenantLineInnerInterceptor
 */
public class DataScopeInnerInterceptor implements InnerInterceptor {

    private static final Log logger = LogFactory.getLog(DataScopeInnerInterceptor.class);

    private DataScopeHandler dataScopeHandler;

    public DataScopeInnerInterceptor(DataScopeHandler dataScopeHandler) {
        this.dataScopeHandler = dataScopeHandler;
    }

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

        // 原始SQL
        String originalSql = mpBs.sql();
        if (logger.isDebugEnabled()) {
            logger.debug("Original SQL: " + originalSql);
        }

        try {
            Statement statement = CCJSqlParserUtil.parse(originalSql);

            if (statement instanceof Select) {
                Select select = (Select) statement;
                // 解析SQL
                this.processSelect(select);
                final String parserSQL = statement.toString();
                mpBs.sql(parserSQL);
                if (logger.isDebugEnabled()) {
                    logger.debug("parser sql: " + parserSQL);
                }
            }
        } catch (JSQLParserException e) {
            throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e, originalSql);
        }
    }

    protected void processSelect(Select select) {
        processSelectBody(select.getSelectBody());
        List<WithItem> withItemsList = select.getWithItemsList();
        if (!CollectionUtils.isEmpty(withItemsList)) {
            withItemsList.forEach(this::processSelectBody);
        }
    }

    protected void processSelectBody(SelectBody selectBody) {
        if (selectBody == null) {
            return;
        }
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            // With关键字
            WithItem withItem = (WithItem) selectBody;
            /**
             * jsqlparser 4.3版本 使用 {@code withItem.getSubSelect().getSelectBody())} 代替 {@code withItem.getSelectBody()}
             */
            processSelectBody(withItem.getSelectBody());
        } else {
            // 集合操作 UNION(并集) MINUS(差集)
            SetOperationList operationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = operationList.getSelects();
            if (CollectionUtils.isNotEmpty(selectBodyList)) {
                selectBodyList.forEach(this::processSelectBody);
            }
        }
    }

    /**
     * 处理 PlainSelect
     */
    protected void processPlainSelect(PlainSelect plainSelect) {
        DataScope dataScope = DataScopeContextHolder.get();
        if (dataScope == null) {
            return;
        } else {
            DataScopeType type = dataScope.type();
            try {
                switch (type) {
                    case DATA_SCOPE_ALL:
                        dataScopeHandler.setWhereForAll(plainSelect, dataScope);
                        break;
                    case DATA_SCOPE_CUSTOM:
                        dataScopeHandler.setWhereForCustom(plainSelect, dataScope);
                        break;
                    case DATA_SCOPE_DEPT:
                        dataScopeHandler.setWhereForDept(plainSelect, dataScope);
                        break;
                    case DATA_SCOPE_DEPT_AND_CHILD:
                        dataScopeHandler.setWhereForDeptAndChild(plainSelect, dataScope);
                        break;
                    case DATA_SCOPE_SELF:
                        dataScopeHandler.setWhereForSelf(plainSelect, dataScope);
                        break;
                }
            } catch (JSQLParserException e) {
                throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e);
            }
        }

    }

}



5.DataScopeHandler

package com.example.datascope.handler;

import com.example.datascope.annotation.DataScope;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.statement.select.PlainSelect;

/**
 * 数据权限处理逻辑
 * 参考: https://gitee.com/baomidou/mybatis-mate-examples/blob/master/mybatis-mate-datascope/src/main/java/mybatis/mate/datascope/config/DataScopeConfig.java
 */
public interface DataScopeHandler {
    /**
     * 全部数据权限
     *
     * @param plainSelect
     * @param dataScope   数据范围注解
     * @throws JSQLParserException SQL解析异常
     */
    default void setWhereForAll(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException {
        // do nothing
    }

    /**
     * 自定数据权限
     *
     * @param plainSelect
     * @param dataScope   数据范围注解
     * @throws JSQLParserException SQL解析异常
     */
    default void setWhereForCustom(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException {
        throw new UnsupportedOperationException("暂不支持的数据权限类型");
    }

    /**
     * 部门数据权限
     *
     * @param plainSelect
     * @param dataScope   数据范围注解
     * @throws JSQLParserException SQL解析异常
     */
    void setWhereForDept(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException;

    /**
     * 部门及以下数据权限
     *
     * @param plainSelect
     * @param dataScope   数据范围注解
     * @throws JSQLParserException SQL解析异常
     */
    void setWhereForDeptAndChild(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException;

    /**
     * 仅本人数据权限
     *
     * @param plainSelect
     * @param dataScope   数据范围注解
     * @throws JSQLParserException SQL解析异常
     */
    void setWhereForSelf(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException;

}

三、插件集成

  • DataScopeHandler接口

根据实际情况,结合用户表和部门表结构,自实现DataScopeHandler接口,demo如下
可参考 mybatis-mate-datascope

@Configuration
public class DataScopeHandlerAutoConfigure {

    @Bean
    DataScopeHandler dataScopeHandler() {
        return new DataScopeHandler() {

            // TODO 从上下文中获取用户ID和部门ID

            @Override
            public void setWhereForDept(PlainSelect plainSelect, DataScope dataScope) {
                String column = String.format("%s.dept_id", dataScope.deptAlias());
                EqualsTo expression = new EqualsTo();
                expression.setLeftExpression(new Column(column));
                expression.setRightExpression(new StringValue("1"));
                setWhere(plainSelect, expression);
            }

            @Override
            public void setWhereForDeptAndChild(PlainSelect plainSelect, DataScope dataScope) throws JSQLParserException {
                String sqlPart = String.format("%s.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = '%s' or find_in_set( '%s' , ancestors ) )", dataScope.deptAlias(), "1", "1");
                Expression expression = CCJSqlParserUtil.parseCondExpression(sqlPart);
                setWhere(plainSelect, expression);
            }

            @Override
            public void setWhereForSelf(PlainSelect plainSelect, DataScope dataScope) {
                String column = String.format("%s.user_id", dataScope.userAlias());
                EqualsTo expression = new EqualsTo();
                expression.setLeftExpression(new Column(column));
                expression.setRightExpression(new LongValue(1L));
                setWhere(plainSelect, expression);
            }

            private void setWhere(PlainSelect plainSelect, Expression expression) {
                Expression where = plainSelect.getWhere();
                if (where == null) {
                    // 不存在 where 条件
                    plainSelect.setWhere(new Parenthesis(expression));
                } else {
                    // 存在 where 条件 and 处理
                    plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), expression));
                }
            }

        };
    }

}


  • 启用DataScopeInnerInterceptor插件
@Configuration
public class MybatisPlusAutoConfigure {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new DataScopeInnerInterceptor(dataScopeHandler));
        return interceptor;
    }
}

  • 启用@DataScope AOP注解扫描(默认启用)
datascope
  enable: true

四、使用示例

@DataScope(type = DataScopeType.DATA_SCOPE_DEPT_AND_CHILD, userAlias = "u", deptAlias = "d")

五、参考

TenantLineInnerInterceptor

mybatis-mate-datascope

RuoYi-Vue

Mybatis-Plus入门系列(18) -基于注解的动态数据权限实现方案

<think>嗯,用户想了解MyBatisPlus拦截器用于数据范围限制的适用性,以及有没有更好的替代方案和最佳实践。首先,我得回忆一下MyBatisPlus的拦截器机制。通常,拦截器可以用于在SQL执行前后插入自定义逻辑,比如分页、数据权限控制。数据范围限制,比如根据用户角色动态添加WHERE条件,拦截器确实是一个常见做法。 然后,用户提到替代方案,可能需要考虑其他方法是否更优。比如,使用MyBatisPlus自带的Tenant Line多租户插件,这个内置的功能可能比自定义拦截器更高效,因为它专门为数据隔离设计,避免了自己写拦截器可能带来的维护问题。另外,动态SQL也是一种选择,通过注解或者XML文件动态拼接条件,可能更灵活,但需要开发者在每个查询中手动添加,可能会增加代码量。 还有,Spring AOP也是一个可能的替代方案,通过切面编程在Service层进行数据过滤,这样不侵入持久层,但需要考虑事务和性能的影响。另外,Shiro或Spring Security的权限控制可能结合业务逻辑进行前置过滤,不过这属于应用层的控制,和数据库层面的拦截器不同。 最佳实践方面,可能需要根据场景选择。比如多租户用Tenant Line,简单场景用动态SQL,复杂权限用AOP或者拦截器。同时需要注意性能,比如拦截器过多会影响执行效率,缓存策略是否合理,以及如何维护清晰的代码结构。 然后,我需要检查用户提供的引用内容。引用[1]提到MyBatis-Plus的整合,引用[2]是MyBatisPlus的版本,引用[3]是关于数据淘汰策略,可能不太相关。所以主要参考前两个引用,特别是MyBatisPlus的官方文档部分,确保提到的功能是存在的。 另外,用户可能是在实际项目中遇到了数据权限的问题,比如不同用户只能看到自己部门的数据,而使用拦截器可能存在维护困难或性能问题,所以想找更好的方法。需要强调不同方案的优缺点,帮助用户根据具体情况选择。 总结来说,回答需要包括拦截器的适用场景、替代方案及其优缺点、最佳实践建议,并引用提供的参考资料中的相关部分,比如MyBatisPlus的版本和整合方式。</think>### MyBatis-Plus 数据范围限制方案解析 #### 一、MyBatis-Plus 拦截器方案适用性分析 通过自定义拦截器实现数据范围限制是常见做法,适用于以下场景: 1. **全局统一控制**:自动为所有查询添加租户ID、部门ID等固定条件[^1] 2. **动态条件注入**:根据用户权限动态拼接WHERE子句(如:`dept_id IN (1,2,3)`) 3. **敏感数据过滤**:强制过滤逻辑删除标记字段(`is_deleted=0`) **代码示例**: ```java @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class DataScopeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 动态注入数据过滤条件 MetaObject metaObject = SystemMetaObject.forObject(invocation.getArgs()[1]); metaObject.setValue("deptIdList", SecurityUtils.getCurrentUserDeptIds()); return invocation.proceed(); } } ``` #### 二、替代方案对比 | 方案 | 适用场景 | 优点 | 缺点 | |------|---------|------|------| | **Tenant Line 插件** | 多租户隔离 | 官方支持,配置简单 | 仅支持等值条件 | | **动态SQL** | 简单条件过滤 | 灵活性高 | 需要手动维护 | | **AOP 切面** | 业务层过滤 | 解耦持久层 | 可能影响事务 | | **视图封装** | 复杂权限模型 | 权限集中管理 | 性能损耗较大 | **推荐方案选择路径**: ``` 简单租户隔离 → Tenant Line 动态权限条件 → 拦截器 + 注解 复杂业务规则 → AOP + 服务层过滤 ``` #### 三、最佳实践建议 1. **性能优化**: - 在拦截器中添加`@Component`和`@Order`控制执行顺序[^2] - 对静态表(如字典表)设置白名单 ```java @Component @Order(1) // 确保在分页插件前执行 public class CustomInterceptor implements Interceptor { ... } ``` 2. **缓存策略**: ```java // 使用Guava缓存权限数据 LoadingCache<String, List<Long>> deptCache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<>() { public List<Long> load(String key) { return permissionService.getAccessibleDepts(key); } }); ``` 3. **安全增强**: - 结合`@DataScope`注解定义过滤规则 - 使用ThreadLocal存储当前用户上下文 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataScope { String deptAlias() default ""; } ``` #### 四、注意事项 1. **版本兼容性**:确保使用MyBatisPlus 3.4+版本以获得最新特性支持 2. **SQL注入防护**:所有动态参数必须使用预编译方式 3. **监控机制**:通过`@Autowired SqlSessionFactory`获取运行时统计信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬山境KL攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值