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 中的 deptAliasuserAlias 必须与 SQL 中的表别名一致,否则生成的条件将引用错误的别名。
  • CUSTOM 权限范围需要通过 DataPermissionContext.setCustomDeptIds() 设置部门 ID 列表,未设置时将生成 dept_id = -1 的永假条件。
  • 拦截器通过 JSQLParser 解析 SQL,对于极端复杂的 SQL 可能存在兼容性问题。