最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
数据权限为何需要置于Mapper XML中:DataScope拦截器的架构权衡
时间:2026-05-28 18:45:01 编辑:袖梨 来源:一聚教程网
Forge Admin的数据权限模块通过forge-starter-datascope实现多维度的数据隔离,本文将深入解析其在Mapper XML层的技术实现与工程考量。

1. 这个问题在企业后台里为什么常见
企业后台系统常面临多角色数据隔离需求:
- 部门经理:仅可查看本部门数据
- 区域总监:可访问区域内所有部门数据
- 超级管理员:具备全公司数据访问权限
- 跨组织协作:需特殊授权才能访问其他部门项目数据
传统实现方式存在三大典型方案:
方案一:Controller 层硬编码(最差实践)
@GetMapping("/orders")
public Result> getOrders() {
Long userId = getCurrentUserId();
User user = userService.getById(userId);
List orders;
if (user.isSuperAdmin()) {
orders = orderService.list(); // 全部数据
} else if (user.isDeptManager()) {
orders = orderService.listByDept(user.getDeptId()); // 本部门
} else {
orders = orderService.listByUser(userId); // 个人数据
}
return Result.ok(orders);
}
缺陷:权限逻辑重复分散,接口维护成本高。
方案二:Service 层统一处理(中等实践)
@Service
public class OrderService {
public List<Order> list() {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
// 权限判断逻辑
DataScopeContext context = getDataScopeContext();
if (context.isDeptScope()) {
wrapper.eq("dept_id", context.getDeptId());
} else if (context.isUserScope()) {
wrapper.eq("create_by", context.getUserId());
}
// ... 其他条件
return orderMapper.selectList(wrapper);
}
}
缺陷:SQL与业务代码强耦合,复杂查询处理困难。
方案三:AOP 或拦截器(当前方案)
// 只需一个注解
@GetMapping("/orders")
@DataScope(deptAlias = "o", deptField = "dept_id")
public Result> getOrders(PageParam param) {
// 业务代码完全不用关心权限
return Result.ok(orderService.page(param));
}
挑战:需解决SQL自动改写、分页兼容等技术难点。
2. Forge Admin 是怎么解决的
forge-starter-datascope采用MyBatis拦截器机制,在SQL执行前自动追加权限过滤条件。
2.1 整体架构
用户请求 → Controller → Service → Mapper
↓
MybatisPlusInterceptor
↓
┌─────────────┼──────────────┐
│ │ │
DataScope TenantLine Pagination
Interceptor Interceptor Interceptor
│
├→ 1. 检查是否需要跳过权限
├→ 2. 查询当前用户的数据权限上下文
├→ 3. 获取当前 Mapper 方法的权限配置
├→ 4. 根据权限类型构建 SQL 条件
└→ 5. 使用 JSQLParser 改写原 SQL
2.2 七种数据权限范围
模块预置七种标准权限类型:
| 权限类型 | 代码 | 说明 | SQL 条件示例 |
|---|---|---|---|
| ALL | 1 | 全部数据 | 无附加条件 |
| SELF | 2 | 个人数据 | user_id = 123 |
| ORG | 3 | 本组织 | org_id IN (101, 102) |
| ORG_AND_CHILD | 4 | 本组织及子组织 | org_id IN (101, 102, 103, 104) |
| CUSTOM | 5 | 自定义组织 | org_id IN (自定义组织ID列表) |
| TENANT_ALL | 6 | 本租户 | tenant_id = 1001 |
| REGION | 7 | 本行政区划 | area_code = '440300' OR area_code IN (子区划) |
2.3 核心模块组成
forge-starter-datascope/
├── config/
│ ├── DataScopeAutoConfiguration.java # 自动配置
│ ├── DataScopeProperties.java # 配置属性
│ └── DataScopeIgnore.java # @DataScopeIgnore 注解
├── interceptor/
│ └── DataScopeInterceptor.java # 核心拦截器
├── context/
│ ├── DataScopeContext.java # 权限上下文
│ └── DataScopeContextHolder.java # 上下文持有者
├── service/
│ ├── IDataScopeService.java # 服务接口
│ └── impl/DataScopeServiceImpl.java # 服务实现
├── enums/
│ └── DataScopeType.java # 权限类型枚举
└── entity/ # 数据库实体
├── SysDataScopeConfig.java # Mapper 权限配置
└── SysRoleDataScope.java # 角色-自定义组织关联
3. 核心数据结构与配置协议
3.1 权限配置表(sys_data_scope_config)
采用Mapper方法级配置策略:
CREATE TABLE sys_data_scope_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
mapper_id VARCHAR(500) NOT NULL COMMENT 'Mapper方法全限定名',
table_alias VARCHAR(50) COMMENT '表别名',
user_id_column VARCHAR(50) COMMENT '用户ID字段名',
org_id_column VARCHAR(50) COMMENT '组织ID字段名',
tenant_id_column VARCHAR(50) COMMENT '租户ID字段名',
region_code_column VARCHAR(50) COMMENT '行政区划代码字段名',
enabled TINYINT DEFAULT 1 COMMENT '是否启用',
UNIQUE KEY uk_mapper (mapper_id)
);
典型配置示例:
| mapper_id | table_alias | org_id_column | 说明 |
|---|---|---|---|
com.example.mapper.OrderMapper.selectList | o | dept_id | 订单列表按部门过滤 |
com.example.mapper.UserMapper.selectPage | u | org_id | 用户列表按组织过滤 |
com.example.mapper.ReportMapper.getRegionStats | r | area_code | 报表按行政区划过滤 |
3.2 权限上下文(DataScopeContext)
拦截器通过该对象获取用户权限信息:
public class DataScopeContext {
private Long userId; // 当前用户ID
private List<Long> orgIds; // 用户所属组织ID列表
private List<Long> roleIds; // 用户角色ID列表
private Integer minDataScope; // 最小数据权限值(值越小权限越大)
private Set<Long> customOrgIds; // 自定义组织ID集合
private Long tenantId; // 租户ID
private String regionCode; // 行政区划代码
private Integer regionLevel; // 行政区划级别
private String regionAncestors; // 行政区划祖先路径
}
3.3 权限计算规则
核心规则:多角色时取最小data_scope值
-- 用户角色表 sys_user_role
user_id | role_id
--------|--------
1001 | 1 -- 角色1: data_scope = 3 (本组织)
1001 | 2 -- 角色2: data_scope = 4 (本组织及子组织)-- 最终权限: MIN(3, 4) = 3 (本组织)
特殊处理:data_scope=5时需检查自定义组织配置
// DataScopeType.getByRoleDataScope()
public static DataScopeType getByRoleDataScope(Integer code, boolean hasCustomOrgIds) {
return switch (code) {
case 5 -> hasCustomOrgIds ? CUSTOM : SELF; // 关键兼容点
// ... 其他 case
};
}
4. 核心实现链路
4.1 第一步:拦截器入口
@Component
public class DataScopeInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
// 1. 检查跳过标记(后台任务等场景)
if (DataScopeContextHolder.isSkip()) {
return;
}
// 2. 获取当前 Mapper 方法ID
String mapperId = ms.getId();
// 3. 处理分页 Count 查询(去掉 _mpCount 后缀)
if (mapperId.endsWith("_mpCount")) {
mapperId = mapperId.replace("_mpCount", "");
}
// 4. 查询权限配置(带缓存)
SysDataScopeConfig config = dataScopeService.getDataScopeConfig(mapperId);
if (config == null || config.getEnabled() == 0) {
return; // 未配置或已禁用
}
// 5. 获取用户权限上下文
DataScopeContext context = dataScopeService.getCurrentUserDataScope();
if (context == null) {
return; // 未登录或后台任务
}
// 6. 确定权限类型
DataScopeType scopeType = DataScopeType.getByRoleDataScope(
context.getMinDataScope(),
!CollectionUtils.isEmpty(context.getCustomOrgIds())
);
// 7. 根据权限类型改写 SQL
String originalSql = boundSql.getSql();
String modifiedSql = buildDataScopeSql(originalSql, config, context, scopeType);
// 8. 替换 BoundSql 中的 SQL
PLUGIN_UTILS.MPBoundSql mpBoundSql = PLUGIN_UTILS.mpBoundSql(boundSql);
mpBoundSql.sql(modifiedSql);
}
}
4.2 第二步:SQL 改写引擎
基于JSQLParser实现SQL解析与改写:
private String buildDataScopeSql(String originalSql, SysDataScopeConfig config,
DataScopeContext context, DataScopeType scopeType) {
// 1. 解析 SQL 为抽象语法树(AST)
Statement statement = CCJSqlParserUtil.parse(originalSql);
if (!(statement instanceof Select)) {
return originalSql; // 只处理 SELECT 查询
}
// 2. 获取 SELECT 主体
Select select = (Select) statement;
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 3. 构建权限条件表达式
Expression dataScopeCondition = buildDataScopeCondition(config, context, scopeType);
// 4. 追加到 WHERE 子句
Expression where = plainSelect.getWhere();
if (where != null) {
// 原 WHERE 条件 AND 权限条件
plainSelect.setWhere(new AndExpression(where, dataScopeCondition));
} else {
plainSelect.setWhere(dataScopeCondition);
}
// 5. 序列化回 SQL 字符串
return select.toString();
}
4.3 第三步:条件构建策略
按权限类型生成对应SQL条件:
private Expression buildDataScopeCondition(SysDataScopeConfig config,
DataScopeContext context,
DataScopeType scopeType) {
switch (scopeType) {
case SELF:
// user_id = 123
return buildSimpleCondition(config.getTableAlias(),
config.getUserIdColumn(),
context.getUserId());
case ORG:
// org_id IN (101, 102)
return buildInCondition(config.getTableAlias(),
config.getOrgIdColumn(),
context.getOrgIds());
case ORG_AND_CHILD:
// org_id IN (101, 102, 103, 104...) 包含所有子孙组织
List allOrgIds = expandOrgTree(context.getOrgIds());
return buildInCondition(config.getTableAlias(),
config.getOrgIdColumn(),
allOrgIds);
case CUSTOM:
// org_id IN (自定义组织列表)
return buildInCondition(config.getTableAlias(),
config.getOrgIdColumn(),
context.getCustomOrgIds());
case TENANT_ALL:
// tenant_id = 1001
return buildSimpleCondition(config.getTableAlias(),
config.getTenantIdColumn(),
context.getTenantId());
case REGION:
// area_code = '440300' OR area_code IN (子区划)
return buildRegionCondition(config, context);
case ALL:
default:
return null; // 无附加条件
}
}
4.4 第四步:复杂场景处理
场景一:JOIN 查询
-- 原始 SQL
SELECT o.*, u.name
FROM t_order o
LEFT JOIN t_user u ON o.user_id = u.id
WHERE o.status = 'ACTIVE'-- 配置: table_alias = "o", org_id_column = "dept_id"
-- 用户权限: 部门经理,只能看部门101的数据-- 改写后 SQL
SELECT o.*, u.name
FROM t_order o
LEFT JOIN t_user u ON o.user_id = u.id
WHERE o.status = 'ACTIVE'
AND o.dept_id = 101 -- 自动追加
场景二:子查询
-- 原始 SQL
SELECT * FROM t_order
WHERE id IN (
SELECT order_id FROM t_order_item WHERE price > 100
)-- 改写后 (SELF 权限)
SELECT * FROM t_order o
WHERE o.create_by = 12345 -- 自动追加
AND id IN (
SELECT order_id FROM t_order_item WHERE price > 100
)
场景三:分页查询
自动处理MyBatis-Plus分页插件的Count查询:
// 原始方法: OrderMapper.selectPage
// 生成的 Count 方法: OrderMapper.selectPage_mpCount// 拦截器处理
if (mapperId.endsWith("_mpCount")) {
actualMapperId = mapperId.replace("_mpCount", ""); // 还原为原始方法
}
// 使用相同的配置进行权限过滤,确保分页统计准确
5. 关键工程取舍
5.1 为什么选择 Mapper XML 层?
三大实现层面对比:
| 实现层 | 优点 | 缺点 | Forge Admin 的选择 |
|---|---|---|---|
| Controller 层 | 业务语义清晰 | 1. 代码重复 2. 易遗漏 3. 难以统一维护 | 否决 |
| Service 层 | 逻辑集中,可复用 | 1. SQL 与业务耦合 2. 复杂查询难处理 3. 分页统计困难 | 否决 |
| Mapper XML 层 | 1. SQL 透明化 2. 统一入口 3. 与业务解耦 4. 自动处理分页 | 1. 配置复杂 2. 调试困难 3. SQL 兼容性问题 | 选择 |
选择依据:
- 透明化:业务代码无需感知权限逻辑
- 统一性:确保所有数据访问都经过权限检查
- 兼容性:与MyBatis-Plus生态无缝集成
- 性能:通过优化使性能开销最小化
5.2 性能优化策略
两级缓存机制
// 一级缓存:权限配置(30分钟)
private final Cache configCache =
Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(1000)
.build();// 二级缓存:组织树展开结果(10分钟)
private final Cache> orgChildCache =
Caffeine.newBuilder()
相关文章