1. 概述

1.1 什么是数据权限?

数据权限是一种细粒度的权限控制机制,它控制用户能够查看和操作哪些数据。与功能权限(控制用户能访问哪些功能)不同,数据权限关注的是数据行级别的访问控制

1.2 应用场景

  • 部门隔离:销售人员只能查看自己部门的客户数据

  • 层级管理:部门经理可以查看本部门及下属部门的数据

  • 个人数据:员工只能查看自己创建的数据

  • 自定义范围:管理员可以指定特定部门的数据访问权限

1.3 系统特点

透明化:基于 MyBatis Plus 插件,自动在 SQL 中添加权限条件,业务代码无感知 ✅ 灵活性:支持注解控制,可以启用/禁用特定规则 ✅ 高性能:使用上下文缓存,避免重复计算 ✅ 可扩展:支持自定义数据权限规则


2. 核心原理

2.1 工作流程

┌─────────────┐
│  用户请求    │
└──────┬──────┘
       │
       ▼
┌─────────────────────────┐
│  1. 解析 @DataPermission │ 
│     注解(AOP 拦截)      │
└──────┬──────────────────┘
       │
       ▼
┌─────────────────────────┐
│  2. 执行 Service 方法    │
│     调用 Mapper 查询     │
└──────┬──────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│  3. MyBatis Plus 拦截器拦截 SQL  │
│     (DataPermissionInterceptor) │
└──────┬──────────────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│  4. 获取当前用户的数据权限信息   │
│     (从 LoginUser Context)      │
└──────┬──────────────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│  5. 根据权限规则动态生成 SQL 条件│
│     (DeptDataPermissionRule)    │
└──────┬──────────────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│  6. 将权限条件拼接到原始 SQL     │
│     WHERE ... AND (dept_id IN ?)│
└──────┬──────────────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│  7. 执行最终 SQL,返回结果       │
└─────────────────────────────────┘

2.2 SQL 改写示例

原始 SQL:

SELECT * FROM auction_info WHERE status = 1

添加数据权限后的 SQL:

-- 场景1:仅本人数据
SELECT * FROM auction_info WHERE status = 1 AND user_id = 100
​
-- 场景2:本部门数据
SELECT * FROM auction_info WHERE status = 1 AND dept_id IN (10, 11, 12)
​
-- 场景3:本部门 + 本人数据
SELECT * FROM auction_info WHERE status = 1 AND (dept_id IN (10, 11, 12) OR user_id = 100)
​
-- 场景4:全部数据(不添加任何条件)
SELECT * FROM auction_info WHERE status = 1

2.3 核心组件

组件 说明
@DataPermission 注解,用于控制数据权限的启用/禁用
DataPermissionRule 数据权限规则接口,定义如何生成 SQL 条件
DeptDataPermissionRule 基于部门的数据权限规则实现
DataPermissionRuleHandler 权限规则处理器,负责调用规则生成 SQL
DataPermissionInterceptor MyBatis 拦截器,拦截 SQL 并添加权限条件
DeptDataPermissionRespDTO 数据权限响应对象,包含用户的权限信息

3. 架构设计

3.1 类图关系

┌──────────────────────────────┐
│ DataPermissionConfiguration  │  配置类
│ - rentDeptDataPermissionRuleCustomizer()
└────────────┬─────────────────┘
             │ 配置
             ▼
┌──────────────────────────────┐
│ DeptDataPermissionRule       │  权限规则
│ - getTableNames()            │
│ - getExpression()            │
│ - addDeptColumn()            │
│ - addUserColumn()            │
└────────────┬─────────────────┘
             │ 实现
             ▼
┌──────────────────────────────┐
│ DataPermissionRule           │  规则接口
│ + getTableNames()            │
│ + getExpression()            │
└──────────────────────────────┘
             ▲
             │ 被调用
             │
┌──────────────────────────────┐
│ DataPermissionRuleHandler    │  规则处理器
│ - getSqlSegment()            │
└────────────┬─────────────────┘
             │ 注入到
             ▼
┌──────────────────────────────┐
│ DataPermissionInterceptor    │  MyBatis 拦截器
│ (MyBatis Plus Plugin)        │
└──────────────────────────────┘

3.2 执行流程详解

步骤1:配置阶段(启动时)

DataPermissionConfiguration 中配置需要数据权限控制的表:

@Bean
public DeptDataPermissionRuleCustomizer rentDeptDataPermissionRuleCustomizer() {
    return rule -> {
        // 为竞拍信息表配置部门字段
        rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
        
        // 为用户表配置部门字段和用户字段
        rule.addDeptColumn(SUserDO.class, "dept_id");
        rule.addUserColumn(SUserDO.class, "id");
    };
}
步骤2:查询阶段(运行时)

当执行数据库查询时:

  1. MyBatis Plus 拦截器捕获 SQL

    // DataPermissionRuleHandler.getSqlSegment()
    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId)
  2. 获取适用的权限规则

    List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
  3. 生成权限条件表达式

    Expression allExpression = rule.getExpression(tableName, table.getAlias());
  4. 拼接到原始 SQL

    allExpression = new AndExpression(allExpression, oneExpress);
步骤3:权限计算

DeptDataPermissionRule.getExpression() 方法的执行逻辑:

@Override
public Expression getExpression(String tableName, Alias tableAlias) {
    // 1. 获取登录用户
    LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
    
    // 2. 获取用户的数据权限信息(带缓存)
    DeptDataPermissionRespDTO deptDataPermission = 
        loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
    if (deptDataPermission == null) {
        deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
        loginUser.setContext(CONTEXT_KEY, deptDataPermission);
    }
    
    // 3. 根据权限信息生成 SQL 条件
    if (deptDataPermission.getAll()) {
        return null; // 全部数据,不添加条件
    }
    
    // 4. 构建部门条件:WHERE dept_id IN (...)
    Expression deptExpression = buildDeptExpression(...);
    
    // 5. 构建用户条件:WHERE user_id = ?
    Expression userExpression = buildUserExpression(...);
    
    // 6. 组合条件:WHERE (dept_id IN (...) OR user_id = ?)
    return new OrExpression(deptExpression, userExpression);
}

4. 数据权限范围

4.1 数据范围枚举

系统定义了5种数据权限范围(DataScopeEnum):

枚举值 代码 说明 SQL 条件示例
ALL 1 全部数据权限 无条件
DEPT_CUSTOM 2 指定部门数据权限 dept_id IN (1, 2, 3)
DEPT_ONLY 3 仅本部门数据 dept_id = 10
DEPT_AND_CHILD 4 本部门及子部门数据 dept_id IN (10, 11, 12, 13)
SELF 5 仅本人数据 user_id = 100

4.2 权限计算逻辑

权限计算在 PermissionServiceImpl.getDeptDataPermission() 方法中实现:

@Override
@DataPermission(enable = false) // 防止递归
public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) {
    // 1. 获得用户的所有角色
    List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
    
    // 2. 初始化结果对象
    DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO();
    
    // 3. 如果没有角色,只能查看自己的数据
    if (CollUtil.isEmpty(roles)) {
        result.setSelf(true);
        return result;
    }
    
    // 4. 遍历角色,累加权限(取并集)
    for (RoleDO role : roles) {
        switch (role.getDataScope()) {
            case 1: // ALL - 全部数据
                result.setAll(true);
                break;
                
            case 2: // DEPT_CUSTOM - 指定部门
                result.getDeptIds().addAll(role.getDataScopeDeptIds());
                result.getDeptIds().add(userDeptId.get()); // 加上自己的部门
                break;
                
            case 3: // DEPT_ONLY - 本部门
                result.getDeptIds().add(userDeptId.get());
                break;
                
            case 4: // DEPT_AND_CHILD - 本部门及子部门
                result.getDeptIds().addAll(
                    deptService.getChildDeptIdListFromCache(userDeptId.get())
                );
                result.getDeptIds().add(userDeptId.get());
                break;
                
            case 5: // SELF - 仅本人
                result.setSelf(true);
                break;
        }
    }
    
    return result;
}

4.3 权限合并规则

当用户拥有多个角色时,权限采用并集(OR)策略:

  • 如果任一角色拥有 ALL 权限 → 可查看全部数据

  • 如果角色A可查看部门1,角色B可查看部门2 → 可查看部门1和部门2

  • 如果任一角色设置了 SELF → 可查看自己的数据

示例:

用户张三同时拥有两个角色:

  • 角色A:本部门数据(部门10)

  • 角色B:指定部门数据(部门20、30)

最终权限:

result.getDeptIds() = [10, 20, 30]

生成的 SQL:

WHERE dept_id IN (10, 20, 30)

5. 配置使用

5.1 快速开始

步骤1:创建配置类

在你的模块中创建 DataPermissionConfiguration 类:

package com.sunrise.system.module.rent.framework.web.config.core;

import com.sunrise.system.module.rent.dal.dataobject.auctioninfo.AuctionInfoDO;
import com.sunrise.system.module.rent.dal.dataobject.tradeinfo.TradeinfoDO;
import com.sunrise.system.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * rent 模块的数据权限配置
 */
@Configuration("rentDataPermissionConfiguration")
public class DataPermissionConfiguration {

    @Bean("rentDeptDataPermissionRuleCustomizer")
    public DeptDataPermissionRuleCustomizer rentDeptDataPermissionRuleCustomizer() {
        return rule -> {
            // 配置需要数据权限的表
            rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            rule.addDeptColumn(TradeinfoDO.class, "dept_id");
        };
    }
}
步骤2:确保数据库表有相应字段

需要数据权限的表必须包含以下字段之一:

  • dept_id:部门编号字段(用于部门权限)

  • user_idcreator:用户编号字段(用于个人权限)

CREATE TABLE auction_info (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    dept_id BIGINT,        -- 部门字段
    creator BIGINT,        -- 创建者字段
    -- 其他字段...
);
步骤3:验证配置

启动项目后,执行查询操作,查看日志中的 SQL:

-- 自动添加了数据权限条件
SELECT * FROM auction_info WHERE status = 1 AND dept_id IN (10, 20)

5.2 配置方法详解

addDeptColumn() - 配置部门字段
// 方法1:使用默认字段名 "dept_id"
rule.addDeptColumn(AuctionInfoDO.class);

// 方法2:指定自定义字段名
rule.addDeptColumn(AuctionInfoDO.class, "department_id");

// 方法3:直接使用表名
rule.addDeptColumn("t_auction_info", "dept_id");

生成的 SQL 条件:

WHERE dept_id IN (10, 20, 30)
addUserColumn() - 配置用户字段
// 方法1:使用默认字段名 "user_id"
rule.addUserColumn(SUserDO.class);

// 方法2:指定自定义字段名(如 creator、create_by 等)
rule.addUserColumn(SUserDO.class, "creator");

// 方法3:直接使用表名
rule.addUserColumn("system_user", "id");

生成的 SQL 条件:

WHERE user_id = 100
同时配置部门和用户字段
rule.addDeptColumn(SUserDO.class, "dept_id");
rule.addUserColumn(SUserDO.class, "id");

生成的 SQL 条件(OR 关系):

WHERE (dept_id IN (10, 20) OR id = 100)

5.3 多模块配置

不同模块可以有各自的数据权限配置,使用唯一的 Bean 名称避免冲突:

System 模块:

@Configuration
public class DataPermissionConfiguration {
    
    @Bean("systemDeptDataPermissionRuleCustomizer")
    public DeptDataPermissionRuleCustomizer systemCustomizer() {
        return rule -> {
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
            rule.addUserColumn(AdminUserDO.class, "id");
        };
    }
}

Rent 模块:

@Configuration("rentDataPermissionConfiguration")
public class DataPermissionConfiguration {
    
    @Bean("rentDeptDataPermissionRuleCustomizer")
    public DeptDataPermissionRuleCustomizer rentCustomizer() {
        return rule -> {
            rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            rule.addDeptColumn(TradeinfoDO.class, "dept_id");
        };
    }
}

5.4 多模块共享配置(重要⭐)

核心原理

关键发现: Spring Boot 的自动配置会自动收集所有模块DeptDataPermissionRuleCustomizer Bean,然后统一应用!

查看 RsDeptDataPermissionAutoConfiguration 的源码:

@Bean
public DeptDataPermissionRule deptDataPermissionRule(
        PermissionCommonApi permissionApi,
        List<DeptDataPermissionRuleCustomizer> customizers) {  // 👈 注意这里是 List
    
    DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
    
    // 遍历所有模块的 customizer,统一应用
    customizers.forEach(customizer -> customizer.customize(rule));
    
    return rule;
}

这意味着: 你不需要手动合并配置!每个模块定义自己的 DeptDataPermissionRuleCustomizer,框架会自动合并它们。

方案一:各模块独立配置(推荐)

适用场景: 各模块相对独立,每个模块管理自己的表

项目结构:
├── rs-module-system
│   └── DataPermissionConfiguration (配置 AdminUserDO, DeptDO)
├── rs-module-rent  
│   └── DataPermissionConfiguration (配置 AuctionInfoDO, TradeinfoDO)
└── rs-module-crm
    └── DataPermissionConfiguration (配置 CrmCustomerDO, CrmContractDO)

System 模块:

@Configuration
public class DataPermissionConfiguration {
    @Bean
    public DeptDataPermissionRuleCustomizer systemCustomizer() {
        return rule -> {
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
        };
    }
}

Rent 模块:

@Configuration("rentDataPermissionConfiguration")
public class DataPermissionConfiguration {
    @Bean("rentCustomizer")
    public DeptDataPermissionRuleCustomizer rentCustomizer() {
        return rule -> {
            rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            rule.addDeptColumn(TradeinfoDO.class, "dept_id");
        };
    }
}

CRM 模块:

@Configuration("crmDataPermissionConfiguration")
public class DataPermissionConfiguration {
    @Bean("crmCustomizer")
    public DeptDataPermissionRuleCustomizer crmCustomizer() {
        return rule -> {
            rule.addDeptColumn(CrmCustomerDO.class, "dept_id");
            rule.addDeptColumn(CrmContractDO.class, "dept_id");
        };
    }
}

最终效果: 所有配置自动合并,生效的表包括:

  • admin_user, dept (System模块)

  • auction_info, trade_info (Rent模块)

  • crm_customer, crm_contract (CRM模块)

优点:

  • 各模块配置独立,互不干扰

  • 模块可以单独启用/禁用

  • 配置代码就近管理

方案二:公共模块统一配置

适用场景: 多个模块使用相同的表,或者希望集中管理配置

步骤1:创建公共配置模块

rs-module-common
└── src/main/java/.../common/config/CommonDataPermissionConfiguration.java

步骤2:在公共模块中配置

package com.sunrise.system.module.common.config;
​
import com.sunrise.system.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * 公共数据权限配置
 * 适用于多个模块共享的表
 */
@Configuration
public class CommonDataPermissionConfiguration {
    
    @Bean("commonDeptDataPermissionRuleCustomizer")
    public DeptDataPermissionRuleCustomizer commonCustomizer() {
        return rule -> {
            // 配置所有模块都需要的通用表
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
            rule.addUserColumn(AdminUserDO.class, "id");
            
            // 配置业务模块共享的表
            rule.addDeptColumn(CommonBusinessDO.class, "dept_id");
        };
    }
}

步骤3:业务模块仍可添加自己的配置

// Rent 模块仍然可以配置自己特有的表
@Configuration("rentDataPermissionConfiguration")
public class DataPermissionConfiguration {
    @Bean("rentCustomizer")
    public DeptDataPermissionRuleCustomizer rentCustomizer() {
        return rule -> {
            // 只配置 Rent 模块特有的表
            rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            rule.addDeptColumn(TradeinfoDO.class, "dept_id");
        };
    }
}

优点:

  • 公共表集中管理,避免重复配置

  • 业务模块可以额外添加自己的配置

  • 配置层次清晰

方案三:单一配置类(不推荐)

适用场景: 小型项目,表数量少

@Configuration
public class GlobalDataPermissionConfiguration {
    
    @Bean
    public DeptDataPermissionRuleCustomizer globalCustomizer() {
        return rule -> {
            // System 模块的表
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
            
            // Rent 模块的表
            rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            rule.addDeptColumn(TradeinfoDO.class, "dept_id");
            
            // CRM 模块的表
            rule.addDeptColumn(CrmCustomerDO.class, "dept_id");
            
            // ... 所有表都配置在这里
        };
    }
}

缺点:

  • 所有模块耦合在一起

  • 无法单独启用/禁用模块

  • 配置文件过于庞大

实际运行示例

假设项目有以下配置:

// System 模块
@Bean
public DeptDataPermissionRuleCustomizer systemCustomizer() {
    return rule -> {
        rule.addDeptColumn(AdminUserDO.class);  // admin_user 表
    };
}
​
// Rent 模块
@Bean
public DeptDataPermissionRuleCustomizer rentCustomizer() {
    return rule -> {
        rule.addDeptColumn(AuctionInfoDO.class);  // auction_info 表
    };
}
​
// CRM 模块
@Bean
public DeptDataPermissionRuleCustomizer crmCustomizer() {
    return rule -> {
        rule.addDeptColumn(CrmCustomerDO.class);  // crm_customer 表
    };
}

Spring 启动时的执行过程:

// 1. Spring 收集所有的 DeptDataPermissionRuleCustomizer Bean
List<DeptDataPermissionRuleCustomizer> customizers = [
    systemCustomizer,
    rentCustomizer,
    crmCustomizer
];
​
// 2. 创建 DeptDataPermissionRule
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
​
// 3. 依次应用所有 customizer
systemCustomizer.customize(rule);  // 添加 admin_user
rentCustomizer.customize(rule);    // 添加 auction_info
crmCustomizer.customize(rule);     // 添加 crm_customer
​
// 4. 最终 rule 包含所有表的配置
rule.getTableNames() = ["admin_user", "auction_info", "crm_customer"]

查询任何一个表时,都会自动应用数据权限:

// 查询用户表
userMapper.selectList(null);
// 生成 SQL: SELECT * FROM admin_user WHERE dept_id IN (10, 20)
​
// 查询竞拍表
auctionMapper.selectList(null);
// 生成 SQL: SELECT * FROM auction_info WHERE dept_id IN (10, 20)
​
// 查询客户表
customerMapper.selectList(null);
// 生成 SQL: SELECT * FROM crm_customer WHERE dept_id IN (10, 20)
配置优先级和覆盖

问题: 如果多个模块对同一张表配置了不同的字段,会怎样?

// System 模块
rule.addDeptColumn("user_table", "dept_id");
​
// Rent 模块
rule.addDeptColumn("user_table", "department_id");  // 不同的字段名

答案: 后面的配置会覆盖前面的配置(Map 的特性)

// 最终 user_table 使用的字段是 "department_id"

建议: 避免多个模块配置同一张表,如果必须配置,请在公共模块中统一管理。

调试技巧

如果想查看最终生效了哪些表的配置,可以在启动时打印日志:

@Configuration
public class DataPermissionConfiguration {
    
    @Bean
    public DeptDataPermissionRuleCustomizer rentCustomizer(
            @Lazy DeptDataPermissionRule rule) {  // 延迟注入
        
        return customRule -> {
            customRule.addDeptColumn(AuctionInfoDO.class, "dept_id");
            customRule.addDeptColumn(TradeinfoDO.class, "dept_id");
            
            // 打印日志
            log.info("[数据权限配置] Rent 模块配置完成");
        };
    }
    
    // 可选:添加一个 CommandLineRunner 查看最终配置
    @Bean
    public CommandLineRunner printDataPermissionConfig(DeptDataPermissionRule rule) {
        return args -> {
            log.info("[数据权限配置] 最终生效的表: {}", rule.getTableNames());
        };
    }
}

启动日志示例:

[数据权限配置] System 模块配置完成
[数据权限配置] Rent 模块配置完成
[数据权限配置] CRM 模块配置完成
[数据权限配置] 最终生效的表: [admin_user, dept, auction_info, trade_info, crm_customer, crm_contract]
总结
方案 适用场景 Bean名称 优点 缺点
各模块独立配置 大中型项目 每个模块不同 解耦、灵活 需要多个配置类
公共+业务配置 有共享表的项目 公共+业务 层次清晰 稍复杂
单一配置 小型项目 单一 简单直接 耦合度高

推荐做法:

  • ✅ 使用方案一(各模块独立配置)

  • ✅ 给每个 Bean 起不同的名称,避免冲突

  • ✅ 框架会自动合并所有配置,无需手动处理

  • ✅ 可以根据需要随时添加/删除模块配置


6. 实战示例

6.1 示例1:竞拍信息的数据权限

场景: 销售人员只能查看自己部门的竞拍信息

配置:

rule.addDeptColumn(AuctionInfoDO.class, "dept_id");

业务代码(无需修改):

@Service
public class Auction          InfoServiceImpl extends ServiceImpl<AuctionInfoMapper, AuctionInfoDO> 
    implements AuctionInfoService {
    
    @Override
    public List<AuctionInfoDO> getAuctionList(AuctionInfoQueryVO query) {
        // 直接查询,框架会自动添加数据权限
        return baseMapper.selectList(new LambdaQueryWrapper<AuctionInfoDO>()
            .eq(AuctionInfoDO::getStatus, query.getStatus())
        );
    }
}

执行的 SQL:

-- 假设当前用户在部门10,且角色配置为"本部门数据"
SELECT * FROM auction_info 
WHERE status = 1 
  AND dept_id = 10  -- 自动添加

6.2 示例2:用户数据(部门 + 个人)

场景: 用户可以查看自己部门的用户,也可以查看自己的信息

配置:

rule.addDeptColumn(SUserDO.class, "dept_id");
rule.addUserColumn(SUserDO.class, "id");

执行的 SQL:

-- 假设当前用户ID=100,部门ID=10
SELECT * FROM s_user 
WHERE status = 1 
  AND (dept_id = 10 OR id = 100)  -- 自动添加

6.3 示例3:禁用特定方法的数据权限

场景: 管理员统计功能需要查看所有数据,不受数据权限限制

使用注解禁用:

@Service
public class AuctionInfoServiceImpl implements AuctionInfoService {
    
    // 方法1:使用 @DataPermission 注解
    @Override
    @DataPermission(enable = false)
    public Long getTotalCount() {
        // 此方法不受数据权限限制
        return baseMapper.selectCount(null);
    }
    
    // 方法2:使用工具类
    @Override
    public Long getTotalCount2() {
        return DataPermissionUtils.executeIgnore(() -> {
            // 此代码块不受数据权限限制
            return baseMapper.selectCount(null);
        });
    }
}

执行的 SQL:

-- 不会添加数据权限条件
SELECT COUNT(*) FROM auction_info

6.4 示例4:排除特定规则

场景: 某些查询只使用用户权限,不使用部门权限

@Service
public class AuctionInfoServiceImpl implements AuctionInfoService {
    
    @Override
    @DataPermission(excludeRules = DeptDataPermissionRule.class)
    public List<AuctionInfoDO> getMyAuctions() {
        // 只添加用户权限条件,不添加部门权限条件
        return baseMapper.selectList(null);
    }
}

6.5 示例5:仅使用特定规则

@Service
public class AuctionInfoServiceImpl implements AuctionInfoService {
    
    @Override
    @DataPermission(includeRules = DeptDataPermissionRule.class)
    public List<AuctionInfoDO> getDeptAuctions() {
        // 只添加部门权限条件
        return baseMapper.selectList(null);
    }
}

7. 高级用法

7.1 自定义数据权限规则

除了内置的 DeptDataPermissionRule,你可以实现自己的权限规则:

步骤1:实现 DataPermissionRule 接口
@Slf4j
@AllArgsConstructor
public class CustomDataPermissionRule implements DataPermissionRule {
    
    @Override
    public Set<String> getTableNames() {
        // 返回需要应用此规则的表名
        return Sets.newHashSet("custom_table");
    }
    
    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 获取当前登录用户
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        
        // 自定义权限逻辑
        // 例如:根据用户的自定义属性过滤数据
        String customField = (String) loginUser.getContext().get("customField");
        
        // 构建 SQL 条件:WHERE custom_field = ?
        return new EqualsTo(
            MyBatisUtils.buildColumn(tableName, tableAlias, "custom_field"),
            new StringValue(customField)
        );
    }
}
步骤2:注册规则
@Configuration
public class DataPermissionConfiguration {
    
    @Bean
    public CustomDataPermissionRule customDataPermissionRule() {
        return new CustomDataPermissionRule();
    }
}

7.2 动态控制数据权限

在代码中临时禁用数据权限
// 方式1:使用 Runnable
DataPermissionUtils.executeIgnore(() -> {
    // 这里的代码不受数据权限限制
    userMapper.selectList(null);
});
​
// 方式2:使用 Callable(有返回值)
List<UserDO> users = DataPermissionUtils.executeIgnore(() -> {
    return userMapper.selectList(null);
});
​
// 方式3:手动控制
DataPermissionUtils.addDisableDataPermission();
try {
    // 执行查询
    userMapper.selectList(null);
} finally {
    DataPermissionUtils.removeDataPermission();
}

7.3 上下文缓存优化

数据权限信息会缓存在 LoginUser 的上下文中,避免重复计算:

// 首次获取,会调用 permissionApi.getDeptDataPermission()
DeptDataPermissionRespDTO permission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
​
// 后续获取,直接从上下文读取
if (permission == null) {
    permission = permissionApi.getDeptDataPermission(loginUser.getId());
    loginUser.setContext(CONTEXT_KEY, permission); // 缓存
}

7.4 SQL 表别名支持

系统支持表别名,即使使用别名查询也能正确添加权限条件:

-- 原始 SQL
SELECT a.* FROM auction_info a WHERE a.status = 1
​
-- 添加权限后
SELECT a.* FROM auction_info a WHERE a.status = 1 AND a.dept_id IN (10, 20)

7.5 复杂查询场景

多表关联查询
-- 原始 SQL
SELECT a.*, b.name 
FROM auction_info a 
LEFT JOIN trade_info b ON a.trade_id = b.id
WHERE a.status = 1
​
-- 添加权限后(两个表都配置了数据权限)
SELECT a.*, b.name 
FROM auction_info a 
LEFT JOIN trade_info b ON a.trade_id = b.id
WHERE a.status = 1 
  AND a.dept_id IN (10, 20)  -- auction_info 的权限
  AND b.dept_id IN (10, 20)  -- trade_info 的权限
子查询
-- 原始 SQL
SELECT * FROM auction_info 
WHERE id IN (SELECT auction_id FROM bid_record WHERE amount > 1000)
​
-- 添加权限后
SELECT * FROM auction_info 
WHERE id IN (
    SELECT auction_id FROM bid_record 
    WHERE amount > 1000 
      AND dept_id IN (10, 20)  -- 如果 bid_record 也配置了权限
)
AND dept_id IN (10, 20)  -- auction_info 的权限

8. 常见问题

8.1 为什么我的数据权限不生效?

可能原因:

  1. 表没有配置权限

    // 检查是否添加了配置
    rule.addDeptColumn(YourDO.class, "dept_id");
  2. 表缺少必要字段

    -- 确保表有 dept_id 或 user_id 字段
    ALTER TABLE your_table ADD COLUMN dept_id BIGINT;
  3. 用户角色没有配置数据权限

    • 进入系统管理 → 角色管理

    • 编辑角色,设置"数据权限"字段

  4. 方法被标记为禁用数据权限

    // 检查是否有此注解
    @DataPermission(enable = false)
  5. 非管理员用户

    // 数据权限只对 UserType = ADMIN 生效
    if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
        return null; // 不添加权限条件
    }

8.2 如何调试数据权限?

方法1:开启 SQL 日志

application.yml 中配置:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

查看控制台输出的完整 SQL。

方法2:断点调试

在以下位置打断点:

  • DeptDataPermissionRule.getExpression() - 查看权限条件生成过程

  • DataPermissionRuleHandler.getSqlSegment() - 查看 SQL 拦截过程

  • PermissionServiceImpl.getDeptDataPermission() - 查看权限计算过程

方法3:日志输出

@Override
public Expression getExpression(String tableName, Alias tableAlias) {
    // ...
    log.info("[数据权限] 表名={}, 权限={}", tableName, JsonUtils.toJsonString(deptDataPermission));
    // ...
}

8.3 用户切换部门后,能否看到原部门的数据?

默认行为: 不能看到

系统采用的是方案1:dept_id 不修改,导致用户看不到之前的数据。

解决方案:

如果需要支持查看原部门数据,有以下选择:

方案A:数据迁移(推荐)

-- 编写脚本,将用户的历史数据迁移到新部门
UPDATE auction_info SET dept_id = 20 WHERE creator = 100;

方案B:同时使用 dept_id 和 user_id

// 配置时同时添加部门和用户字段
rule.addDeptColumn(AuctionInfoDO.class, "dept_id");
rule.addUserColumn(AuctionInfoDO.class, "creator");
​
// 生成的 SQL:
// WHERE (dept_id IN (20) OR creator = 100)
// 用户可以看到新部门的数据 + 自己创建的所有数据

方案C:自定义权限规则

实现自己的 DataPermissionRule,使用更复杂的逻辑。

8.4 如何实现"查看下属数据"的权限?

场景: 经理可以查看直接下属创建的数据

方案:

  1. 数据库设计

    ALTER TABLE your_table ADD COLUMN creator BIGINT; -- 创建人
  2. 自定义权限规则

    public class SubordinateDataPermissionRule implements DataPermissionRule {
        
        @Override
        public Expression getExpression(String tableName, Alias tableAlias) {
            // 获取当前用户的所有下属ID
            List<Long> subordinateIds = getSubordinateIds(loginUser.getId());
            
            // 生成条件:WHERE creator IN (下属ID列表)
            return new InExpression(
                MyBatisUtils.buildColumn(tableName, tableAlias, "creator"),
                new ParenthesedExpressionList(new ExpressionList<>(
                    subordinateIds.stream().map(LongValue::new).collect(Collectors.toList())
                ))
            );
        }
    }

8.5 性能优化建议

  1. 字段索引

    -- 为权限字段添加索引
    CREATE INDEX idx_dept_id ON auction_info(dept_id);
    CREATE INDEX idx_user_id ON auction_info(user_id);
  2. 权限缓存

    • 系统已内置缓存,避免重复计算

    • 权限信息存储在 LoginUser.context

  3. 避免过度使用

    • 对于统计、报表等查询,考虑禁用数据权限

    @DataPermission(enable = false)
    public StatisticsVO getStatistics() { ... }
  4. 合理设计表结构

    • 避免在大表上频繁 JOIN

    • 考虑数据归档策略

8.6 数据权限与功能权限的区别

维度 功能权限 数据权限
控制对象 功能(接口、按钮) 数据(数据行)
实现方式 @PreAuthorize 注解 SQL 自动改写
判断时机 进入方法前 执行 SQL 时
示例 是否可以"删除用户" 可以删除"哪些用户"

8.7 递归问题

问题:getDeptDataPermission() 方法中查询数据库时,会再次触发数据权限检查,导致递归。

解决方案:

@Override
@DataPermission(enable = false) // 必须禁用
public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) {
    // 查询用户角色等信息
    // 如果不禁用,会再次调用此方法,导致 StackOverflowError
}

8.8 如何在特定条件下禁用数据权限?

场景: 超级管理员查看所有数据

方案1:在权限规则中判断

@Override
public Expression getExpression(String tableName, Alias tableAlias) {
    LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
    
    // 超级管理员不受限制
    if (loginUser.isSuperAdmin()) {
        return null;
    }
    
    // 普通用户添加权限条件
    return buildExpression(...);
}

方案2:在 Service 中判断

public List<UserDO> getUserList() {
    LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
    
    if (loginUser.isSuperAdmin()) {
        // 超级管理员:禁用数据权限
        return DataPermissionUtils.executeIgnore(() -> {
            return userMapper.selectList(null);
        });
    } else {
        // 普通用户:启用数据权限
        return userMapper.selectList(null);
    }
}

9. 总结

9.1 核心要点

配置简单:只需在配置类中添加 addDeptColumn()addUserColumn()自动生效:无需修改业务代码,MyBatis 拦截器自动添加 SQL 条件 ✅ 灵活控制:支持 @DataPermission 注解控制单个方法的权限规则 ✅ 性能优化:内置缓存机制,避免重复计算权限 ✅ 可扩展:支持自定义权限规则,满足复杂业务场景

9.2 最佳实践

  1. 合理规划表结构

    • 在设计阶段就考虑数据权限字段(dept_idcreator 等)

    • 为权限字段添加索引

  2. 明确权限边界

    • 区分功能权限和数据权限

    • 统计、报表类功能考虑禁用数据权限

  3. 充分测试

    • 测试不同角色的数据可见性

    • 测试边界情况(无角色、多角色等)

  4. 监控和优化

    • 开启 SQL 日志,观察权限条件是否正确

    • 分析慢查询,优化权限相关的查询

9.3 快速检查清单

配置数据权限时,请确认:

  • 表中存在 dept_iduser_id 字段
  • DataPermissionConfiguration 中配置了表和字段
  • 角色的"数据权限"字段已正确设置
  • 用户已分配角色且角色已启用
  • 方法没有被标记为 @DataPermission(enable = false)
  • 用户类型是 ADMIN(默认只对管理员生效)

附录

A. 相关文件清单

文件路径 说明
DataPermissionConfiguration.java 数据权限配置类
DeptDataPermissionRule.java 部门数据权限规则实现
DataPermissionRule.java 数据权限规则接口
DataPermissionRuleHandler.java 权限规则处理器
@DataPermission 数据权限注解
DataPermissionUtils.java 数据权限工具类
DeptDataPermissionRespDTO.java 数据权限响应对象
DataScopeEnum.java 数据范围枚举

Logo

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

更多推荐