1. 前言

在多租户或多部门系统中,我们希望开发者在写 SQL(如 SELECT * FROM sys_user)时,不需要手动拼接 WHERE tenant_id = 1WHERE dept_id = 100

本文将基于 MyBatis-Plus 的拦截器机制,结合 JSqlParserSpring AOP,实现一套灵活的、支持注解控制的数据权限框架。

2. 核心架构设计

整个方案可以分为三个层级:

  1. 策略层 (Rule):定义具体的过滤规则(如部门规则、租户规则)。
  2. 执行层 (Handler & Factory):负责在 SQL 执行时,将规则拼接进 SQL。
  3. 控制层 (Annotation & AOP):负责通过注解动态开启、关闭或过滤规则。

核心交互时序图:

DataPermissionRule (具体规则) RuleFactory (决策工厂) RuleHandler (执行工头) ContextHolder (上下文) AnnotationInterceptor (AOP 切面) 业务调用 (Service) DataPermissionRule (具体规则) RuleFactory (决策工厂) RuleHandler (执行工头) ContextHolder (上下文) AnnotationInterceptor (AOP 切面) 业务调用 (Service) 阶段一:环境准备 (AOP) 阶段二:SQL 拦截与改写 (MyBatis-Plus) alt [匹配成功] [不匹配] loop [遍历每一个规则] 阶段三:清理现场 (Finally) 1. 调用业务方法 (e.g., selectUser) 1 解析 @DataPermission 注解 2 2. 入栈配置 (add) (Context 存入 ThreadLocal) 3 3. 执行 SQL (触发 MP 拦截器 getSqlSegment) 4 4.以此 SQL ID 获取生效规则 5 5. 获取当前注解配置 6 返回 Annotation (或 null) 7 核心决策逻辑 (判空/Enable/Include/Exclude) 8 6. 返回规则列表 [Rule A, Rule B...] 9 7. 表名匹配吗? (getTableNames) 10 8. 生成 SQL 片段 (getExpression) 11 返回 "dept_id = 100" 12 9. 拼接 AND 条件 13 跳过 14 10. 返回最终 WHERE 条件 15 业务执行结束 16 11. 出栈清理 (remove) 17 清理完成 18

3. 核心代码实现

第一步:定义规则接口 (Strategy Pattern)

我们需要一个接口来统一所有的数据权限规则。

public interface DataPermissionRule {
    /**
     * 获取该规则生效的表名集合
     * 作用:快速过滤,只有匹配的表才需要处理,提升性能
     */
    Set<String> getTableNames();

    /**
     * 根据表名和别名,生成 SQL 条件表达式
     * @param tableName 表名 (e.g., sys_user)
     * @param tableAlias 别名 (e.g., u)
     * @return JSqlParser 表达式 (e.g., u.dept_id = 1)
     */
    Expression getExpression(String tableName, Alias tableAlias);
}

第二步:实现具体的业务规则 (Rule Implementation)

以“部门数据权限”为例,限制用户只能查询本部门的数据。

@AllArgsConstructor
public class DeptDataPermissionRule implements DataPermissionRule {

    private final PermissionApi permissionApi; // 业务 Service,用于获取当前用户的部门权限

    @Override
    public Set<String> getTableNames() {
        // 配置哪些表需要进行部门隔离
        return Set.of("sys_user", "sys_dept", "t_order");
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 1. 获取当前登录用户
        Long userId = SecurityUtils.getLoginUserId();
        if (userId == null) return null; // 未登录不处理

        // 2. 获取用户的部门数据权限 (通常建议做一层 RequestScope 缓存)
        Set<Long> deptIds = permissionApi.getUserDeptDataPermission(userId);
        if (CollUtil.isEmpty(deptIds)) {
            // 如果没有部门权限,直接返回 null = null,保证查不到数据
            return new EqualsTo(new NullValue(), new NullValue());
        }

        // 3. 构造 SQL:dept_id IN (1, 2, 3)
        // 注意:这里需要根据别名处理列名,如 u.dept_id
        String columnName = MyBatisUtils.getAliasColumn(tableAlias, "dept_id");
        return new InExpression(
            new Column(columnName),
            new ParenthesedExpressionList(new ExpressionList(
                deptIds.stream().map(LongValue::new).collect(Collectors.toList())
            ))
        );
    }
}

第三步:上下文与 AOP 控制 (Context Holder)

为了让业务层能通过注解灵活控制权限(如 @DataPermission(enable=false)),我们需要 ThreadLocal 上下文。

3.1 上下文持有者

public class DataPermissionContextHolder {
    // 使用 LinkedList 支持方法的嵌套调用(入栈/出栈)
    private static final ThreadLocal<LinkedList<DataPermission>> CONTEXT = 
            ThreadLocal.withInitial(LinkedList::new);

    public static void add(DataPermission annotation) {
        CONTEXT.get().addLast(annotation);
    }

    public static DataPermission remove() {
        LinkedList<DataPermission> list = CONTEXT.get();
        DataPermission last = list.removeLast();
        if (list.isEmpty()) CONTEXT.remove(); // 清理 ThreadLocal,防内存泄露
        return last;
    }

    public static DataPermission get() {
        return CONTEXT.get().peekLast(); // 获取栈顶(当前生效)的配置
    }
}

3.2 AOP 拦截器

@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {

    /**
     * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
     */
    static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);

    @Getter
    private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 入栈
        DataPermission dataPermission = this.findAnnotation(methodInvocation);
        if (dataPermission != null) {
            DataPermissionContextHolder.add(dataPermission);
        }
        try {
            // 执行逻辑
            return methodInvocation.proceed();
        } finally {
            // 出栈
            if (dataPermission != null) {
                DataPermissionContextHolder.remove();
            }
        }
    }

    private DataPermission findAnnotation(MethodInvocation methodInvocation) {
        // 1. 从缓存中获取
        Method method = methodInvocation.getMethod();
        Object targetObject = methodInvocation.getThis();
        Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
        MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
        DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
        if (dataPermission != null) {
            return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
        }

        // 2.1 从方法中获取
        dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
        // 2.2 从类上获取
        if (dataPermission == null) {
            dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
        }
        // 2.3 添加到缓存中
        dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
        return dataPermission;
    }

}

3.3 切面 Advisor

/**
 * DataPermission 注解的 Advisor
 * 职责:将拦截器 (Interceptor) 与切点 (Pointcut) 绑定
 */
@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {

    private final Advice advice;
    private final Pointcut pointcut;

    /**
     * 构造函数
     * @param interceptor 我们在 3.2 节定义的拦截器
     */
    public DataPermissionAnnotationAdvisor(DataPermissionAnnotationInterceptor interceptor) {
        this.advice = interceptor;
        this.pointcut = this.buildPointcut();
    }

    /**
     * 构建切点逻辑 (核心匹配规则)
     */
    protected Pointcut buildPointcut() {
        // 1. 类级别的匹配器:检查类头上是否有 @DataPermission
        // 第二个参数 true 表示 checkInherited,支持继承的注解
        Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);

        // 2. 方法级别的匹配器:检查方法头上是否有 @DataPermission
        // 第一个参数 null 表示不关心类注解,只关心方法注解
        Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);

        // 3. 组合逻辑 (Union)
        // 含义:(类上有注解) OR (方法上有注解) -> 都算命中
        // 使用 Spring 的 ComposablePointcut 进行组合
        return new ComposablePointcut(classPointcut).union(methodPointcut);
    }
}

第四步:规则决策工厂 (Factory)

工厂负责根据当前的 AOP 上下文,筛选出本次 SQL 执行需要哪些规则。

@RequiredArgsConstructor
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {

    private final List<DataPermissionRule> rules; // Spring 注入所有 Rule Bean

    @Override
    public List<DataPermissionRule> getDataPermissionRules() {
        // 1. 获取上下文中的注解配置
        DataPermission activeAnnotation = DataPermissionContextHolder.get();

        // 2. 如果没注解,默认返回所有规则(安全兜底)
        if (activeAnnotation == null) {
            return rules;
        }

        // 3. 如果显式禁用,返回空
        if (!activeAnnotation.enable()) {
            return Collections.emptyList();
        }

        // 4. 处理 Include(白名单)和 Exclude(黑名单)
        // 这里使用 Stream 过滤,逻辑:Include 优先于 Exclude
        return rules.stream().filter(rule -> {
            Class<?> ruleClass = rule.getClass();
            if (ArrayUtil.isNotEmpty(activeAnnotation.includeRules())) {
                return ArrayUtil.contains(activeAnnotation.includeRules(), ruleClass);
            }
            if (ArrayUtil.isNotEmpty(activeAnnotation.excludeRules())) {
                return !ArrayUtil.contains(activeAnnotation.excludeRules(), ruleClass);
            }
            return true;
        }).collect(Collectors.toList());
    }
}

第五步:MyBatis-Plus 处理器 (Handler)

这是连接业务逻辑与 MyBatis 底层的桥梁。它实现 MP 的 MultiDataPermissionHandler 接口。

@RequiredArgsConstructor
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {

    private final DataPermissionRuleFactory ruleFactory;

    @Override
    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
        // 1. 从工厂拿到当前生效的规则列表
        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRules();
        if (CollUtil.isEmpty(rules)) return null;

        Expression allExpression = null;
        
        // 2. 遍历所有规则,拼装 AND 条件
        for (DataPermissionRule rule : rules) {
            // 2.1 只要管辖范围内的表
            if (!rule.getTableNames().contains(table.getName())) {
                continue;
            }

            // 2.2 生成单条条件 (e.g., dept_id = 1)
            Expression oneExpress = rule.getExpression(table.getName(), table.getAlias());
            if (oneExpress == null) continue;

            // 2.3 拼接到总条件中:WHERE (old) AND (rule1) AND (rule2)
            allExpression = allExpression == null ? oneExpress
                    : new AndExpression(allExpression, oneExpress);
        }
        return allExpression;
    }
}

第六步:最终组装与配置 (Configuration)

将上述组件组装起来,并注入到 MyBatis-Plus 的拦截器链中。

@Configuration
public class DataPermissionConfiguration {

	@Bean
    public DataPermissionRule deptDataPermissionRule(PermissionApi permissionApi) {
        DataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
        return rule;
    }

    @Bean
    public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
        return new DataPermissionRuleFactoryImpl(rules);
    }

    @Bean
    public DataPermissionRuleHandler dataPermissionRuleHandler(DataPermissionRuleFactory ruleFactory) {
        return new DataPermissionRuleHandler(ruleFactory);
    }

    /**
     * 配置 MP 的拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(DataPermissionRuleHandler handler) {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 1. 创建数据权限拦截器
        DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
        // 2. 【关键】dataPermissionInterceptor没有使用自动注入,需要手动注入 Handler
        dataPermissionInterceptor.setDataPermissionHandler(handler);
        
        // 3. 添加到拦截器链
        interceptor.addInnerInterceptor(dataPermissionInterceptor);
        // (可选) 添加分页拦截器等...
        
        return interceptor;
    }
    
}


4. 实战指南:如何在业务开发中使用?

框架搭建完毕后,在日常业务开发中,数据权限的使用主要分为三种场景:默认自动生效注解灵活控制编程式临时控制

4.1 场景一:默认无感知过滤 (The Magic)

这是最常见的场景。开发者在编写 Service 代码时,完全不需要关心权限逻辑,就像写普通的 CRUD 一样。

业务代码:

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    public List<UserDO> getUserList() {
        // 开发者只写了查询所有,没加任何 WHERE 条件
        return userMapper.selectList(null); 
    }
}

实际执行的 SQL:
框架会自动识别当前登录人(如租户 ID=1,部门 ID=100),并改写 SQL:

SELECT * FROM sys_user 
WHERE tenant_id = 1       -- 自动拼接租户规则
  AND dept_id = 100       -- 自动拼接部门规则

注意:前提是 sys_user 表必须包含在 DeptDataPermissionRulegetTableNames() 返回集合中。


4.2 场景二:基于注解的策略控制 (Annotation)

当我们需要打破默认规则时(例如:管理员查询、跨部门统计、后台定时任务),使用 @DataPermission 注解是最优雅的方式。

2.1 彻底关闭权限(上帝视角)

适用于后台管理统计、数据迁移、定时任务等场景。

@DataPermission(enable = false) // 关掉所有规则
public List<UserDO> getAllUsersForAdmin() {
    // 执行 SQL: SELECT * FROM sys_user
    // 结果:查出全库所有租户、所有部门的数据
    return userMapper.selectList(null);
}

2.2 排除特定规则(黑名单)

适用于“虽然我是 A 租户的人,但我要查询所有租户的信息,不过我只能看我自己部门的”这种复杂场景。

// 排除租户规则,但保留部门规则(以及其他规则)
@DataPermission(excludeRules = TenantDataPermissionRule.class)
public List<UserDO> getMultiTenantUsers() {
    // 执行 SQL: SELECT * FROM sys_user WHERE dept_id = 100
    // 结果:租户限制消失了,但部门限制还在
    return userMapper.selectList(null);
}

2.3 只启用特定规则(白名单)

适用于非常严格的场景,确保只有核心规则生效,防止其他规则(如刚开发的实验性规则)干扰。

// 只启用部门规则,忽略其他一切
@DataPermission(includeRules = DeptDataPermissionRule.class)
public void onlyDeptLogic() { ... }


4.3 场景三:代码块级临时控制 (Utils)

如果你不能修改 Service 方法的签名(比如是继承的父类方法),或者你只想在方法内部的某一行查询暂时忽略权限,注解就不好用了。这时我们需要一个工具类 DataPermissionUtils

工具类实现 (Util Implementation):

public class DataPermissionUtils {
    public static <T> T executeIgnore(Supplier<T> supplier) {
        // 1. 压入一个“禁用”配置
        DataPermissionContextHolder.add(new DataPermission() {
            public boolean enable() { return false; }
            // ... 其他默认实现
        });
        try {
            // 2. 执行业务逻辑
            return supplier.get();
        } finally {
            // 3. 恢复现场
            DataPermissionContextHolder.remove();
        }
    }
}

业务使用 (Usage):

public void complexBusiness() {
    // 1. 这里的查询受权限控制
    userMapper.selectList(null); 

    // 2. 这里的查询临时忽略权限
    List<UserDO> allData = DataPermissionUtils.executeIgnore(() -> {
        return userMapper.selectList(null); 
    });

    // 3. 这里的查询恢复受权限控制
    userMapper.selectById(1);
}


4.4 前置条件:上下文的填充 (Important!)

这就回到了我们定义的 Rule 实现类。所有的过滤逻辑都依赖于 “当前登录人是谁”

在 Web 环境中,通常通过 Filter 或 Interceptor 在请求开始时,将用户信息放入 ThreadLocal(如 Spring Security 的 SecurityContextHolder)。

如果是在单元测试非 Web 环境中,使用 Service 前必须先模拟登录,否则 Rule 获取不到 UserID 可能会抛出异常或返回空结果。

单元测试示例:

@Test
public void testDataPermission() {
    // 1. Mock 登录用户 (放入 Security 上下文)
    SecurityUtils.mockLoginUser(new LoginUser().setId(1L).setDeptId(100L));

    // 2. 执行 Service,此时 Rule 才能拿到 dept_id = 100
    List<UserDO> users = userService.getUserList();
    
    // 3. 断言 SQL 是否拼接正确
    Assert.assertTrue(users.size() > 0);
}


5. 进阶篇:基于 Customizer 的规则动态扩展

在上一章节的实现中,我们在 DeptDataPermissionRule 内部写死了 Set.of("sys_user", "sys_dept")。在简单的单体项目中这没问题,但在多模块的大型系统中,这违背了 “开闭原则”

  • 痛点:每次新增业务模块(如 order-module),都需要修改底层的 DeptDataPermissionRule 代码来添加新表。
  • 目标:底层框架只提供“过滤能力”,具体的“过滤名单”交由各业务模块自己注册。

我们将采用 Customizer(自定义器)模式 对架构进行重构。

5.1 定义自定义器接口

首先,定义一个回调接口,允许业务模块获取到 Rule 对象,并往里面添加配置。

@FunctionalInterface
public interface DataPermissionRuleCustomizer<T extends DataPermissionRule> {
    void customize(T rule);
}

5.2 重构抽象规则类 (Rule)

抽象类AbstractDataPermissionRule提供建立Expression的基本工具方法,被具体的rule类继承

public abstract class AbstractDataPermissionRule implements DataPermissionRule {

    // 存储配置:Key=表名, Value=规则列表 (一张表可能有多个字段控制,如同时有部门字段和密级字段)
    private final Map<String, List<RuleConfig>> ruleConfigs = new HashMap<>();

    // --- 1. 配置注册区 (供 Customizer 调用) ---

    public void addRule(Class<?> entityClass, String columnName, ConditionTypeEnum type) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        ruleConfigs.computeIfAbsent(tableName, k -> new ArrayList<>())
                .add(new RuleConfig(columnName, type));
    }

    // 快捷方法:注册 IN
    public void addInColumn(Class<?> entityClass, String columnName) {
        addRule(entityClass, columnName, ConditionTypeEnum.IN);
    }

    // 快捷方法:注册 EQUALS
    public void addEqualColumn(Class<?> entityClass, String columnName) {
        addRule(entityClass, columnName, ConditionTypeEnum.EQUALS);
    }

    // 快捷方法:注册右模糊 (用于部门树 dept_tree_path like '0,100,%')
    public void addLikeRightColumn(Class<?> entityClass, String columnName) {
        addRule(entityClass, columnName, ConditionTypeEnum.LIKE_RIGHT);
    }

    @Override
    public Set<String> getTableNames() {
        return ruleConfigs.keySet();
    }

    // --- 2. 核心工具区 (供子类调用) ---

    /**
     * 获取某张表下,指定类型的字段配置
     */
    protected String getColumn(String tableName, ConditionTypeEnum type) {
        List<RuleConfig> configs = ruleConfigs.get(tableName);
        if (configs == null) return null;
        return configs.stream()
                .filter(c -> c.getType() == type)
                .map(RuleConfig::getColumnName)
                .findFirst().orElse(null);
    }

    /**
     * 【万能构建器】根据类型构建 JSqlParser 表达式
     * @param alias 表别名
     * @param columnName 字段名
     * @param type 操作类型
     * @param value 业务数据 (可以是单个值,也可以是集合)
     */
    protected Expression buildExpression(String tableName, Alias alias, String columnName, ConditionTypeEnum type, Object value) {
        if (StrUtil.isEmpty(columnName)) return null;
        if (value == null && type != ConditionTypeEnum.IS_NULL && type != ConditionTypeEnum.IS_NOT_NULL) return null;

        // 1. 处理列名 (e.g. u.dept_id)
        String finalColumn = MyBatisUtils.getGuardedColumn(tableName, alias, columnName);
        Column columnObj = new Column(finalColumn);

        // 2. 根据类型生成表达式
        switch (type) {
            case EQUALS: return new EqualsTo(columnObj, new LongValue(value.toString()));
            case NOT_EQUALS: return new NotEqualsTo(columnObj, new LongValue(value.toString()));

            case IN:
                Collection<?> coll = (Collection<?>) value;
                if (CollUtil.isEmpty(coll)) return null; // 或者返回 1!=1
                ExpressionList items = new ExpressionList(
                        coll.stream().map(v -> new LongValue(v.toString())).collect(Collectors.toList())
                );
                return new InExpression(columnObj, new ParenthesedExpressionList(items));

            case LIKE_RIGHT: // LIKE 'abc%'
                return new LikeExpression()
                        .withLeftExpression(columnObj)
                        .withRightExpression(new StringValue(value.toString() + "%"));

            case GT: return buildBinary(new GreaterThan(), columnObj, value);
            case GE: return buildBinary(new GreaterThanEquals(), columnObj, value);
            case LT: return buildBinary(new MinorThan(), columnObj, value);
            case LE: return buildBinary(new MinorThanEquals(), columnObj, value);

            case IS_NULL: return new IsNullExpression().withLeftExpression(columnObj);

            default: return null;
        }
    }

    private BinaryExpression buildBinary(BinaryExpression expr, Column column, Object value) {
        expr.setLeftExpression(column);
        expr.setRightExpression(new LongValue(value.toString()));
        return expr;
    }

    // 简单的配置内部类
    @Data
    @AllArgsConstructor
    private static class RuleConfig {
        private String columnName;
        private ConditionTypeEnum type;
    }
}

5.3 重构具体规则类

DeptDataPermissionRule 主要重写关键的getExpression方法,该方法与具体业务相关,因此不在抽象类中,其后续被DataPermissionRuleHandler.getSqlSegment()调用,用来添加条件SQL语句。

注意将当前用户的deptDataPermission放到上下文中,因为该用户登录后,后续请求大多都会经过getExpression来获取条件表达式,如果不进行缓存,每次都要去数据库查询,十分耗时。

@AllArgsConstructor
@Slf4j
public class DeptDataPermissionRule extends AbstractDataPermissionRule {

    private final PermissionCommonApi permissionApi;
    protected static final String CONTEXT_KEY = DeptDataPermissionRule .class.getSimpleName();
    static final Expression EXPRESSION_NULL = new NullValue();


    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 只有有登陆用户的情况下,才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户,才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }
        // 获得数据权限
        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
        // 从上下文中拿不到,则调用逻辑进行获取
        if (deptDataPermission == null) {
            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
            if (deptDataPermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中,避免重复计算
            loginUser.setContext(CONTEXT_KEY, deptDataPermission);
        }
        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
        if (deptDataPermission.getAll()) {
            return null;
        }
        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
                && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
        }

        String inColumnName = getColumn(tableName, ConditionTypeEnum.IN);
        String equalColumnName = getColumn(tableName, ConditionTypeEnum.EQUALS);
        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Expression inExpression = buildExpression(tableName, tableAlias, inColumnName, ConditionTypeEnum.IN, deptDataPermission.getDeptIds());
        Expression equalExpression;
        if(deptDataPermission.getSelf())
            equalExpression = buildExpression(tableName, tableAlias, equalColumnName, ConditionTypeEnum.EQUALS, loginUser.getId());
        else
            equalExpression = null;
        if (inExpression == null && equalExpression == null) {
            // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
//                    loginUser.getId(), tableName, tableAlias.getName()));
            return EXPRESSION_NULL;
        }
        if (inExpression == null) {
            return equalExpression;
        }
        if (equalExpression == null) {
            return inExpression;
        }
        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
        return new ParenthesedExpressionList(new OrExpression(inExpression, equalExpression));
    }
}

5.4 业务模块的配置 (Usage)

现在,如果 system 模块想让 sys_user 表支持数据权限,首先需要在 system 模块下写一个配置类注册Custormizer:

@Configuration(proxyBeanMethods = false)
public class SystemDataPermissionConfiguration {

    @Bean
    public DataPermissionRuleCustomizer<MyDeptDataPermissionRule> sysMyDeptDataPermissionRuleCustomizer() {
        return rule -> {
        	//当SQL操作system_users表时,插入In语句system_users.dept_id in (...)
            rule.addInColumn(AdminUserDO.class, "dept_id");
            //当SQL操作system_depts表时,插入In语句system_depts.id in (...)
            rule.addInColumn(DeptDO.class, "id");
            //当SQL操作system_users表时,插入Equals语句system_users.id = ...
            rule.addEqualColumn(AdminUserDO.class, "id");
        };
    }
}

5.5 自动化装配与 Bean 注册 (AutoConfiguration)

最后,我们需要一个自动配置类,将 Customizer 收集起来,注入到 DeptDataPermissionRule 中,并将 Rule 注册为 Spring Bean。

@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {DataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {

    @Bean
    public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionCommonApi,
                                                             List<DataPermissionRuleCustomizer<DeptDataPermissionRule>> customizers) {
        // 创建 MyDeptDataPermissionRule 对象
        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionCommonApi);
        // 补全表配置
        customizers.forEach(customizer -> customizer.customize(rule));
        return rule;
    }

}

6. 扩展:新增数据权限规则并应用(以租户隔离为例)

随着业务发展,单一的部门数据权限可能无法满足所有需求。例如,在 SaaS 场景下,我们还需要实现租户隔离:不同租户的数据必须严格隔离。

6.1 扩展思路

要新增一种数据权限规则(如租户规则),我们需要完成以下四步闭环,它们紧密围绕 Spring 的 IoC 和 Customizer 模式:

  1. 定义新的规则类 (Rule):继承 AbstractDataPermissionRule 类,并实现getExpression方法,包含新规则的业务逻辑。
  2. 定义对应的 Customizer 类:允许业务模块注册表名到新规则中。
  3. 注册Customizer为Bean
  4. 注册新的 Rule Bean:通过 @Configuration@Bean 将新规则注册到 Spring 容器,并注入所有对应的 Customizer 进行配置。

6.2 第一步:定义新的规则类 (TenantDataPermissionRule)

新建一个类 TenantDataPermissionRule,它将负责租户 ID 的获取和 SQL 片段的生成。

// TenantDataPermissionRule.java
@RequiredArgsConstructor // Lombok 注解,自动生成构造函数
public class TenantDataPermissionRule extends AbstractDataPermissionRule {

    private final PermissionCommonApi permissionApi;
    protected static final String CONTEXT_KEY = TenantDataPermissionRule .class.getSimpleName();
    static final Expression EXPRESSION_NULL = new NullValue();

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 只有有登陆用户的情况下,才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户,才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }
        // 获得数据权限
        TenantDataPermissionRespDTO tenantDataPermission = loginUser.getContext(CONTEXT_KEY, TenantDataPermissionRespDTO.class);
        // 从上下文中拿不到,则调用逻辑进行获取
        if (tenantDataPermission == null) {
            tenantDataPermission = permissionApi.getTenantDataPermission(loginUser.getId());
            if (tenantDataPermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中,避免重复计算
            loginUser.setContext(CONTEXT_KEY, tenantDataPermission);
        }
        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
        if (tenantDataPermission.getAll()) {
            return null;
        }
        // 情况二,即不能查看租户,又不能查看自己,则说明 100% 无权限
        if (CollUtil.isEmpty(tenantDataPermission.getTenantIds())
                && Boolean.FALSE.equals(tenantDataPermission.getSelf())) {
            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
        }

        String inColumnName = getColumn(tableName, ConditionTypeEnum.IN);
        String equalColumnName = getColumn(tableName, ConditionTypeEnum.EQUALS);
        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Expression inExpression = buildExpression(tableName, tableAlias, inColumnName, ConditionTypeEnum.IN, deptDataPermission.getTenantIds());
        Expression equalExpression;
        if(tenantDataPermission.getSelf())
            equalExpression = buildExpression(tableName, tableAlias, equalColumnName, ConditionTypeEnum.EQUALS, loginUser.getId());
        else
            equalExpression = null;
        if (inExpression == null && equalExpression == null) {
            // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
            log.warn("[getExpression][LoginUser({}) Table({}/{}) TenantDataPermission({}) 构建的条件为空]",
                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(tenantDataPermission));
//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
//                    loginUser.getId(), tableName, tableAlias.getName()));
            return EXPRESSION_NULL;
        }
        if (inExpression == null) {
            return equalExpression;
        }
        if (equalExpression == null) {
            return inExpression;
        }
        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
        return new ParenthesedExpressionList(new OrExpression(inExpression, equalExpression));
    }
}

6.3 第二步:定义对应的 Customizer 类并注册

@Bean
    public DataPermissionRuleCustomizer<TenantDeptDataPermissionRule> sysTenantDeptDataPermissionRuleCustomizer() {
        return rule -> {
            rule.addInColumn(AdminUserDO.class, "tenant_id");
            rule.addInColumn(TenantDO.class, "id");
            rule.addEqualColumn(AdminUserDO.class, "id");
        };
    }

6.4 第三步:将新 Rule 注册为 Spring Bean 并注入 Customizer

现在,我们需要一个新的 @Configuration 类(或者在原来的AutoConfiguration类中),负责创建 TenantDataPermissionRule 的实例,并将所有 TenantDataPermissionRuleCustomizer 应用到它上面。

@Configuration
@ConditialOnBean(value={DataPermissionRuleCustomizer.class})
public class YudaoTenantDataPermissionAutoConfiguration {

    // 1. 注册 TenantDataPermissionRule Bean
    @Bean
    public TenantDataPermissionRule tenantDataPermissionRule(
            // 2. Spring 自动收集所有实现了 TenantDataPermissionRuleCustomizer 的 Bean
            List<DataPermissionRuleCustomizer<TenantDataPermissionRule>> customizers) {
        
        // 3. 创建 TenantDataPermissionRule 实例 (初始是空的)
        TenantDataPermissionRule rule = new TenantDataPermissionRule();
        
        // 4. 【核心】遍历所有 Customizer,应用它们的定制化配置
        customizers.forEach(customizer -> customizer.customize(rule));
        
        return rule;
    }
}

6.5 自动融合:新规则如何生效?

无需修改 DataPermissionRuleFactoryImplDataPermissionRuleHandler

  1. Factory 自动收集DataPermissionRuleFactoryImpl 的构造函数注入的是 List<DataPermissionRule>。因为它是一个泛型列表,Spring 会自动将你新注册的 TenantDataPermissionRule Bean,连同之前的 DeptDataPermissionRule Bean,一起注入到这个列表中。
  2. Handler 自动执行DataPermissionRuleHandlergetSqlSegment 方法会遍历从 Factory 拿到的所有 DataPermissionRule。当它遇到 TenantDataPermissionRule 时,就会执行其 getTableNames()getExpression() 方法,自动拼接上租户的 WHERE 条件。

Q&A

Advisor的代码不是说,如果类和方法上没有写@DataPermission就不会被DataPermissionAnnotationInterceptor拦截么,没有被DataPermissionAnnotationInterceptor拦截就不会有dataPermission上下文,那么就不会加where

这是一个基于默认安全策略的设计:注解的作用是“申请特权”,而不是“开启防护”。

DataPermissionAnnotationAdvisor 确实只拦截带 @DataPermission 的方法,因此对于普通的业务方法,拦截器不会执行,DataPermissionContextHolder.get() 拿到的结果就是 null。但这恰恰触发了 DataPermissionRuleFactoryImpl 的默认兜底逻辑:它一旦检测到上下文为 null,就会判定为“当前无特殊指示”,从而强制返回所有配置的规则(return rules)。这就确保了开发者在写代码时,什么都不用做就是最安全的(自动拼接所有 WHERE 条件);只有当需要打破规则(如管理员查询 enable=false)时,才需要加注解来生成上下文,指示工厂去“放行”。

核心代码证据如下:

// DataPermissionRuleFactoryImpl.java
@Override
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
    // 1. 对于没加 @DataPermission 的方法,拦截器不执行,这里拿到 null
    DataPermission dataPermission = DataPermissionContextHolder.get();

    // 2. 【关键逻辑】Context 为 null 并非“不处理”,而是“全量处理”
    if (dataPermission == null) {
        return rules; // 返回容器中所有的规则(如租户规则、部门规则等)
    }
    
    // 3. 只有当 dataPermission != null (有注解) 时,才根据注解去排除或禁用规则
    if (!dataPermission.enable()) {
        return Collections.emptyList();
    }
    // ... Include/Exclude 处理
}

DataPermissionRuleHandler的getSqlSegment方法又是被谁调用的呢?为什么只需要重写这个方法就能实现无感添加Where的功能。

getSqlSegment 方法是由 MyBatis-Plus 的内部拦截器 DataPermissionInterceptor**(继承自 BaseMultiTableInnerInterceptor)在 SQL 执行前的拦截阶段调用的。之所以只需重写该方法就能实现无感添加 WHERE 条件,是因为 MyBatis-Plus 利用 JSqlParser 将原始 SQL 解析为抽象语法树(AST),拦截器会遍历这棵树中的所有表,调用你的 getSqlSegment 获取额外的权限条件片段(如 dept_id = 1),然后通过动态改写 AST**,将这些条件以 AND 的方式强制追加到原 SQL 的 WHERE 子句中,最终执行的是改写后的 SQL,从而实现了业务代码无感知的底层数据隔离。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐