ThinkBridge 当前的数据权限仅对 MyBatis 的数据查询方式进行了支持。

MyBatis 集成

本页详细介绍 ThinkBridge 数据权限模块与 MyBatis 的集成细节,包括拦截器机制、SQL 解析流程、SQL 转换示例、性能优化以及高级自定义配置。

仓库坐标

Maven

<dependency>
    <groupId>com.luamas.thinkbridge</groupId>
    <artifactId>adj-mybatis-spring-boot-starter</artifactId>
</dependency>

Gradle

implementation("com.luamas.thinkbridge:adj-mybatis-spring-boot-starter")

接口规范

package com.luamas.thinkbridge.common.mapper.datascope;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;

public interface DataScope {


    /
     * 数据所对应的资源
     * @return 资源标识
     /
    String getResource();

    /
     * 判断当前数据权限范围是否需要管理此表
     * @param tableName 当前需要处理的表名
     * @return 如果当前数据权限范围包含当前表名,则返回 true。,否则返回 false
     */
    boolean includes(String tableName);

    /*
     * 根据表名和表别名,动态生成的 where/or 筛选条件
     * @param tableName 表名
     * @param tableAlias 表别名,可能为空
     * @return 数据规则表达式
     */
    Expression getExpression(String tableName, Alias tableAlias);

}

执行流程

当 MyBatis 执行 Mapper 方法时,数据权限通过多层协作实现权限过滤,整体流程如下:

  1. 用户调用 Mapper 方法

  2. AOP 切面拦截方法,检查是否有 @DataPermission 注解,若存在则解析注解并推入 DataPermissionRuleHolder 栈

  3. MyBatis 生成 SQL 语句

  4. DataPermissionInterceptor 拦截 StatementHandler.prepare() 方法

  5. 调用 DataPermissionHandler.filterDataScopes(mappedStatementId) 获取当前需要控制的 DataScope 列表

    1. 从 DataPermissionRuleHolder 中弹出当前规则

    2. 根据规则的 ignore/includeResources/excludeResources 过滤全局 DataScope

  6. 调用 DataPermissionHandler.ignorePermissionControl() 判断是否跳过处理(缓存命中时跳过)

  7. 遍历 DataScope 列表,调用 DataScopeSqlProcessor.parserSingle()(SELECT) 或 parserMulti()(INSERT/UPDATE/DELETE)

    1. 解析 SQL 的 FROM 子句涉及的每张表

    2. 对每张表调用 DataScope 的 includes(tableName) 判断是否需要处理

    3. 对匹配的表调用 getExpression() 获取过滤表达式

    4. 将所有表达式使用 AND 连接,注入到 SQL 的 WHERE 子句中

  8. 若当前 sql 未命中任何 DataScope,则记录到 MappedStatementIdsWithoutDataScope 中,下次直接跳过

  9. 执行改写后的 SQL

SQL 转换示例

单表查询

原 SQL:

select * from entity where id = ?

转换后:

SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1

多表 JOIN

原 SQL:

SELECT * FROM entity e
RIGHT JOIN entity1 e1 ON e1.id = e.id

转换后:

SELECT * FROM entity e
RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1
WHERE e1.tenant_id = 1
对于 RIGHT JOIN,左表的条件注入到 JOIN 的 ON 中,右表(驱动表)的条件注入到 WHERE 中。

嵌套子查询

原 SQL:

SELECT * FROM entity e
WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)

转换后:

SELECT * FROM entity e
WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)
AND e.tenant_id = 1
子查询和内层 SQL 都会被递归处理,每层各自注入对应的过滤条件。

隐式内连接

原 SQL:

SELECT * FROM entity, entity1 WHERE entity.id = entity1.id

转换后:

SELECT * FROM entity, entity1
WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1

WITH 子句

原 SQL:

with with_as_A as (select * from entity) select * from with_as_A

转换后:

WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1)
SELECT * FROM with_as_A
WITH 子句中定义的临时表名(如 with_as_A)会被自动识别,不会对其注入过滤条件。

UPDATE 和 DELETE

原 SQL:

update entity set name = ? where id = ?
delete from entity where id = ?

转换后:

UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1
DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1

高级 DataScope 实现

下面展示几种不同维度的 DataScope 实现模式,实际使用时可根据业务需求组合。

学校维度

@Component
public class SchoolDataScope implements DataScope {

    public static final String RESOURCE_NAME = "school";
    final String columnId = "school_name";

    @Override
    public String getResource() {
        return RESOURCE_NAME;
    }

    @Override
    public boolean includes(String tableName) {
        // 利用表名前缀匹配
        return tableName.toLowerCase().startsWith("h2student");
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        LoginUser loginUser = LoginUserHolder.get();

        // 用户未登录或没有权限时,1=2 永假
        if (loginUser == null || noSchoolPermission(loginUser)) {
            return new EqualsTo(new LongValue(1), new LongValue(2));
        }

        // 学生维度由 StudentDataScope 处理
        if (loginUser.isStudent()) {
            return null; // 返回 null 表示不干预
        }

        // 生成 IN 条件:school_name IN ('实验中学', '德高中学')
        List<Expression> list = loginUser.getSchoolNameList()
            .stream()
            .map(StringValue::new)
            .collect(Collectors.toList());
        Column column = new Column(tableAlias == null
            ? this.columnId
            : tableAlias.getName() + "." + this.columnId);
        ParenthesedExpressionList expressionList = new ParenthesedExpressionList();
        expressionList.withExpressions(list);
        return new InExpression(column, expressionList);
    }
}

班级维度

@Component
public class ClassDataScope implements DataScope {

    public static final String RESOURCE_NAME = "class";
    final String columnId = "class_name";

    // 使用 TreeSet(CASE_INSENSITIVE_ORDER) 忽略表名大小写
    private final Set<String> tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

    public ClassDataScope() {
        this.tableNames.addAll(Arrays.asList("h2student", "h2teacher"));
    }

    @Override
    public String getResource() {
        return RESOURCE_NAME;
    }

    @Override
    public boolean includes(String tableName) {
        return this.tableNames.contains(tableName);
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        LoginUser loginUser = LoginUserHolder.get();

        if (loginUser == null || noClassPermission(loginUser)) {
            return new EqualsTo(new LongValue(1), new LongValue(2));
        }

        if (loginUser.isStudent()) {
            return null; // 学生维度由 StudentDataScope 处理
        }

        // 生成 IN 条件:class_name IN ('一班', '二班')
        List<Expression> list = loginUser.getClassNameList()
            .stream()
            .map(StringValue::new)
            .collect(Collectors.toList());
        Column column = new Column(tableAlias == null
            ? this.columnId
            : tableAlias.getName() + "." + this.columnId);
        ParenthesedExpressionList expressionList = new ParenthesedExpressionList();
        expressionList.withExpressions(list);
        return new InExpression(column, expressionList);
    }
}

用户维度

@Component
public class StudentDataScope implements DataScope {

    public static final String RESOURCE_NAME = "student";

    // 使用正则匹配表名
    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^h2student*$");

    @Override
    public String getResource() {
        return RESOURCE_NAME;
    }

    @Override
    public boolean includes(String tableName) {
        return TABLE_NAME_PATTERN.matcher(tableName).matches();
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        LoginUser loginUser = LoginUserHolder.get();

        // 未登录时 1=2 永不满足
        if (loginUser == null) {
            return new EqualsTo(new LongValue(1), new LongValue(2));
        }

        // 老师不做学生维度的过滤,由学校/班级维度控制
        if (loginUser.isTeacher()) {
            return null;
        }

        // 学生只能查看自己的数据:WHERE id = ?
        Column column = new Column(tableAlias == null
            ? "id"
            : tableAlias.getName() + "." + "id");
        return new EqualsTo(column, new LongValue(loginUser.getId()));
    }
}

自定义 DataPermissionHandler

默认的 DefaultDataPermissionHandler 提供了基础的过滤功能。如果您需要更复杂的控制逻辑(如管理员直接放行),可以实现自己的 DataPermissionHandler:

@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {

    private final List<DataScope> dataScopes;

    public CustomDataPermissionHandler(List<DataScope> dataScopes) {
        this.dataScopes = dataScopes;
    }

    @Override
    public List<DataScope> dataScopes() {
        return this.dataScopes;
    }

    @Override
    public List<DataScope> filterDataScopes(String mappedStatementId) {
        // 从栈中获取当前 DataPermissionRule
        DataPermissionRule rule = DataPermissionRuleHolder.peek();

        // 管理员直接放行所有数据
        if (LoginUserHolder.isAdmin()) {
            return new ArrayList<>();
        }

        // 根据规则过滤 DataScope
        return filterDataScopes(rule);
    }

    @Override
    public boolean ignorePermissionControl(List<DataScope> dataScopeList,
                                           String mappedStatementId) {
        // 如果 mappedStatementId 在缓存中,跳过处理
        return MappedStatementIdsWithoutDataScope.onAllWithoutSet(
            dataScopeList, mappedStatementId);
    }
}
自定义 DataPermissionHandler 注册为 Spring Bean 后,框架的自动配置会优先使用您的实现。

性能优化

框架内置了 MappedStatementIdsWithoutDataScope 缓存机制以提升性能:

  • 每次 SQL 解析后,如果当前 mappedStatementId 对应的 SQL 中没有任何表被任何 DataScope 匹配,则该 mappedStatementId 会被记录到缓存中

  • 后续该 mappedStatementId 再次执行时,ignorePermissionControl() 直接返回 rue,跳过 SQL 解析,避免不必要的性能开销

  • 缓存基于 ConcurrentHashMap 实现,线程安全

// 核心逻辑 (DataPermissionInterceptor 中)
Integer matchNum = DataScopeMatchNumHolder.pollMatchNum();
List<DataScope> allDataScopes = this.dataPermissionHandler.dataScopes();
if (allDataScopes.size() == filterDataScopes.size() && matchNum != null && matchNum == 0) {
    // 没有任何匹配,记录到缓存中,下次直接跳过
    MappedStatementIdsWithoutDataScope.addToWithoutSet(filterDataScopes, mappedStatementId);
}
该优化对字典表、常量表等不需要数据权限控制的 Mapper 特别有效,首次执行后便不再解析。

注解使用

将 DataScope 注册为 Bean 后,在 Mapper 上使用 @DataPermission 进行配置:

// 类级别:控制该 Mapper 的所有方法
@DataPermission(includeResources = "order")
@Mapper
public interface OrderMapper {
    List<Order> listOrder();

    // 方法级别覆盖:排除 order 资源,使用其他 DataScope
    @DataPermission(excludeResources = "order")
    List<Order> listAllOrder();
}

// 忽略数据权限控制
@DataPermission(ignore = true)
@Mapper
public interface DictMapper {
    List<Dict> listDict();
}

编程式控制

推荐在 Service 层使用编程式 API 进行细粒度的权限控制:

@Service
public class StudentService {

    @Autowired
    private StudentMapper studentMapper;

    // 使用注解在方法上控制
    @DataPermission(includeResources = ClassDataScope.RESOURCE_NAME)
    public List<Student> listStudentOnlyFilterClass() {
        return studentMapper.listStudent();
    }

    // 编程式控制
    public List<Student> listStudentWithSchoolOnly() {
        DataPermissionRule rule = new DataPermissionRule()
            .setIncludeResources(new String[] { SchoolDataScope.RESOURCE_NAME });
        return DataPermissionUtils.executeWithDataPermissionRule(rule,
            () -> studentMapper.listStudent());
    }

    // 忽略所有数据权限
    public List<Student> listAllStudent() {
        return DataPermissionUtils.executeAndIgnoreAll(
            () -> studentMapper.listStudent());
    }
}

规则优先级

当注解和编程式同时存在时,规则优先级由近及远:

  1. 方法内部的编程式规则(优先级最高)

  2. 被调用方法上的 @DataPermission 注解

  3. 调用者的编程式/注解规则(通过栈式嵌套传递)

  4. 全局规则(所有 DataScope 参与控制,优先级最低)

// 外层:全局规则(所有DataScope生效)
List<Student> all = studentMapper.listStudent();

// 编程式规则覆盖
DataPermissionUtils.executeWithDataPermissionRule(
    new DataPermissionRule().setIncludeResources("class"),
    () -> {
        // 内层:使用 class 维度的 DataScope
        List<Student> byClass = studentMapper.listStudent();

        // 嵌套规则覆盖
        DataPermissionUtils.executeAndIgnoreAll(() -> {
            // 最内层:忽略全部数据权限
            List<Student> all2 = studentMapper.listStudent();
        });
    }
);