scaffold-datapermission -- 行级数据权限模块
模块概述
scaffold-datapermission 是基于 MyBatis-Plus 拦截器的行级数据权限控制模块。通过在 SQL 执行前根据当前用户的数据权限范围自动追加过滤条件,实现细粒度的数据访问控制。模块依赖 scaffold-tenant 模块的上下文机制,通过 ThreadLocal 管理当前用户的权限信息,结合 DataPermissionInterceptor 自动改写 SQL。
功能列表
- 5 种数据权限范围(全部/本部门/本部门及以下/仅本人/自定义)
@DataScope注解指定方法级别的权限范围和表别名- ThreadLocal 管理当前用户权限上下文(
DataPermissionContext) - 自动 SQL 改写(SELECT 追加 WHERE 条件)
- 可配置全局开关
权限范围说明
DataPermissionScope 枚举定义了 5 种权限范围:
| 枚举值 | 编码 | 说明 | 生成的 SQL 条件 |
|---|---|---|---|
ALL |
1 | 全部数据 | 不追加条件 |
DEPT |
2 | 本部门数据 | WHERE dept_id = {当前部门ID} |
DEPT_AND_CHILD |
3 | 本部门及以下数据 | WHERE dept_id = {当前部门ID} |
SELF |
4 | 仅本人数据 | WHERE create_by = {当前用户ID} |
CUSTOM |
5 | 自定义数据 | WHERE dept_id IN ({自定义部门ID列表}) |
默认权限范围为 DEPT_AND_CHILD。
注意:DataPermissionScope 为多态状态机,不遵循布尔语义规范。
配置参数
在 application.yml 中以 scaffold.datapermission 为前缀配置:
scaffold:
datapermission:
enabled: true # 是否启用数据权限
ignore: false # 全局忽略开关(true 时所有权限过滤均不生效)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
boolean | true | 是否启用数据权限功能 |
ignore |
boolean | false | 全局忽略开关,设为 true 时所有权限过滤不生效 |
@DataScope 注解使用
@DataScope 标注在 Mapper 方法或 Repository 类上,用于指定数据权限的表别名和默认权限范围。
注解属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
deptAlias |
String | "d" | 部门表别名 |
userAlias |
String | "u" | 用户表别名 |
permission |
DataPermissionScope | DEPT_AND_CHILD | 默认权限范围 |
标注在 Mapper 方法上
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 查询订单列表,使用部门表别名 "o" 进行权限过滤
*/
@DataScope(deptAlias = "o", userAlias = "o", permission = DataPermissionScope.DEPT)
List<Order> selectOrderList(@Param("query") OrderQuery query);
/**
* 查询本人创建的订单
*/
@DataScope(userAlias = "o", permission = DataPermissionScope.SELF)
List<Order> selectMyOrders(@Param("userId") Long userId);
/**
* 不受数据权限限制的查询
* (DataPermissionContext 中设置 ALL 即可,无需额外注解)
*/
List<Order> selectAllOrders();
}
标注在 Repository 类上
@DataScope(deptAlias = "o", userAlias = "o")
public class OrderRepository extends ServiceImpl<OrderMapper, Order> {
// 该 Repository 中所有查询方法均使用 "o" 作为表别名
}
DataPermissionContext API
DataPermissionContext 是基于 ThreadLocal 的权限上下文管理器,用于存储当前请求的权限信息。通常在认证过滤器或自定义拦截器中设置。
// 设置当前用户 ID
DataPermissionContext.setUserId(10001L);
// 设置当前部门 ID
DataPermissionContext.setDeptId(100L);
// 设置权限范围
DataPermissionContext.setScope(DataPermissionScope.DEPT);
// 设置自定义部门 ID 列表(用于 CUSTOM 权限)
DataPermissionContext.setCustomDeptIds(List.of(100L, 200L, 300L));
// 一次性设置所有信息
DataPermissionContext.setAll(10001L, 100L, DataPermissionScope.DEPT_AND_CHILD, null);
// 获取信息
Long userId = DataPermissionContext.getUserId();
Long deptId = DataPermissionContext.getDeptId();
DataPermissionScope scope = DataPermissionContext.getScope();
List<Long> deptIds = DataPermissionContext.getCustomDeptIds();
// 清除(请求结束时调用)
DataPermissionContext.clear();
| 方法 | 说明 |
|---|---|
setUserId(Long) |
设置当前用户 ID |
getUserId() |
获取当前用户 ID |
setDeptId(Long) |
设置当前部门 ID |
getDeptId() |
获取当前部门 ID |
setScope(DataPermissionScope) |
设置权限范围 |
getScope() |
获取权限范围,默认 DEPT_AND_CHILD |
setCustomDeptIds(List<Long>) |
设置自定义部门 ID 列表 |
getCustomDeptIds() |
获取自定义部门 ID 列表,默认空列表 |
setAll(userId, deptId, scope, customDeptIds) |
一次性设置所有信息 |
clear() |
清除所有上下文信息 |
拦截器原理
DataPermissionInterceptor 是 MyBatis-Plus 的 InnerInterceptor 实现,在 SQL 执行前进行拦截和改写:
1. beforeQuery() 触发
2. 从 DataPermissionContext 获取 scope 和 userId
3. 如果 scope == ALL 或 userId == null,直接放行
4. 检查 Mapper 方法是否有 @DataScope 注解
5. 根据注解的 deptAlias 和 userAlias 构建 SQL 条件
6. 使用 JSQLParser 解析 SQL,在 WHERE 子句中追加权限条件
7. 反射修改 BoundSql 中的 SQL 语句
各权限范围生成的条件:
| 范围 | 条件示例 |
|---|---|
| ALL | (不追加) |
| DEPT | d.dept_id = 100 |
| DEPT_AND_CHILD | d.dept_id = 100(当前简化实现) |
| SELF | u.create_by = 10001 |
| CUSTOM | d.dept_id IN (100, 200, 300) |
使用示例
在过滤器中设置权限上下文
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DataPermissionFilter extends OncePerRequestFilter {
@Autowired
private UserRoleService userRoleService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
try {
if (SecurityContextHolderUtils.isAuthenticated()) {
Long userId = SecurityContextHolderUtils.getCurrentUserId();
Long deptId = userRoleService.getUserDeptId(userId);
DataPermissionScope scope = userRoleService.getDataScope(userId);
List<Long> customDeptIds = scope == DataPermissionScope.CUSTOM
? userRoleService.getCustomDeptIds(userId) : null;
DataPermissionContext.setAll(userId, deptId, scope, customDeptIds);
}
chain.doFilter(request, response);
} finally {
DataPermissionContext.clear();
}
}
}
在 Mapper XML 中使用
<select id="selectOrderList" resultType="com.scaffold.order.entity.Order">
SELECT o.* FROM orders o
LEFT JOIN users u ON o.create_by = u.id
WHERE o.status = #{query.status}
<!-- DataPermissionInterceptor 会在此自动追加权限条件 -->
</select>
Service 层动态控制权限范围
@Service
@RequiredArgsConstructor
public class ReportService {
private final OrderMapper orderMapper;
/**
* 管理员查看所有数据
*/
public List<Order> listAllOrders(OrderQuery query) {
DataPermissionContext.setScope(DataPermissionScope.ALL);
return orderMapper.selectOrderList(query);
}
/**
* 查看自定义部门的数据
*/
public List<Order> listCustomDeptOrders(OrderQuery query, List<Long> deptIds) {
DataPermissionContext.setScope(DataPermissionScope.CUSTOM);
DataPermissionContext.setCustomDeptIds(deptIds);
return orderMapper.selectOrderList(query);
}
}
自动配置说明
本模块通过 DataPermissionAutoConfiguration 注册:
| Bean | 条件 | 说明 |
|---|---|---|
DataPermissionInterceptor |
scaffold.datapermission.enabled=true 且存在 MybatisPlusInterceptor 且 classpath 存在 TenantContext |
数据权限拦截器 |
前置依赖:classpath 中必须存在 com.scaffold.tenant.context.TenantContext(即需要引入 scaffold-tenant 模块)。
注意事项
- 本模块依赖
scaffold-tenant模块(通过@ConditionalOnClass感知),必须同时引入。 DataPermissionContext基于 ThreadLocal,在异步线程中权限信息不会自动传递,需手动处理。DataPermissionContext.clear()必须在请求结束时调用(通常在 Filter 的 finally 块中),防止内存泄漏和线程复用时权限错乱。DEPT_AND_CHILD当前简化实现等同于DEPT(仅按dept_id等值过滤),如需完整的部门树过滤,需配合部门表的tree_path字段或递归查询实现。@DataScope中的deptAlias和userAlias必须与 SQL 中的表别名一致,否则生成的条件将引用错误的别名。CUSTOM权限范围需要通过DataPermissionContext.setCustomDeptIds()设置部门 ID 列表,未设置时将生成dept_id = -1的永假条件。- 拦截器通过 JSQLParser 解析 SQL,对于极端复杂的 SQL 可能存在兼容性问题。