行级数据权限开发指南
通用模板。如果项目有专属技能(如 leniu-data-permission ),优先使用。
设计原则
-
对业务透明:数据权限通过拦截器自动注入 SQL 条件,业务代码无需感知。
-
声明式配置:通过注解声明字段映射关系,框架自动拼接过滤条件。
-
可扩展:权限类型(部门、本人、自定义等)可通过枚举或策略模式扩展。
-
安全兜底:未配置权限范围时默认为"仅本人",避免数据泄露。
权限类型设计
类型 标识 SQL 效果 适用场景
全部数据 1 不拼接条件 超管、全局数据查看
自定义权限 2 dept_id IN (角色关联的部门ID)
跨部门协作
本部门 3 dept_id = ?
部门经理
本部门及以下 4 dept_id IN (当前部门及子部门)
上级部门
仅本人 5 created_by = ?
普通员工
部门及以下或本人 6 dept_id IN (...) OR created_by = ?
混合场景
实现模式
架构概览
Controller -> Service (加注解) -> Mapper -> MyBatis 拦截器 | 自动注入 WHERE 条件 | [你的权限处理器] (查询当前用户权限范围)
步骤 1:定义注解
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataPermission { DataColumn[] value(); String joinStr() default "AND"; // 多角色权限连接方式 }
@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DataColumn { String key() default "deptName"; // 占位符关键字 String value() default "dept_id"; // 对应的表字段名 String permission() default ""; // 拥有此权限则不过滤 }
步骤 2:实现 MyBatis 拦截器
@Intercepts({@Signature(type = Executor.class, method = "query", args = {...})}) public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 从线程上下文获取 @DataPermission 注解
// 2. 获取当前用户的角色及数据权限范围
// 3. 根据权限类型拼接 WHERE 条件
// 4. 修改原始 SQL,追加过滤条件
return invocation.proceed();
}
}
步骤 3:在 Service / Mapper 上使用
@Service public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
// 按部门 + 创建人过滤
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "created_by")
})
@Override
public List<OrderVo> listWithPermission(OrderQuery query) {
return orderMapper.selectList(buildWrapper(query));
}
}
步骤 4:确保数据库表有权限字段
CREATE TABLE biz_order ( id BIGINT NOT NULL COMMENT '主键', -- 业务字段 ... dept_id BIGINT DEFAULT NULL COMMENT '所属部门', -- 必须 created_by BIGINT DEFAULT NULL COMMENT '创建人', -- 必须 created_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );
多表关联(使用表别名)
// SQL: SELECT o.*, u.user_name FROM biz_order o LEFT JOIN sys_user u ON ... @DataPermission({ @DataColumn(key = "deptName", value = "o.dept_id"), @DataColumn(key = "userName", value = "o.created_by") }) List<OrderVo> selectWithJoin(@Param("query") OrderQuery query);
临时忽略数据权限
// 使用工具类忽略权限过滤,查全量数据 Long total = [你的权限工具类].ignore(() -> orderService.count());
// 无返回值 [你的权限工具类].ignore(() -> { configService.refreshAll(); return null; });
指定权限标识跳过过滤
// 拥有 order:all 权限的角色不过滤 @DataPermission({ @DataColumn(key = "deptName", value = "dept_id", permission = "order:all") })
选型建议
方案 优点 缺点 适用场景
MyBatis 拦截器 对业务透明、自动注入 依赖 MyBatis 绝大多数 Java Web 项目
AOP + SQL 改写 框架无关 需自行解析 SQL 非 MyBatis 项目
数据库视图 完全透明 难以动态切换 权限固定的场景
应用层过滤 实现简单 性能差(全量查出再过滤) 数据量小
多角色权限计算
-
SELECT 查询:多角色权限用 OR 连接(并集,看到更多数据)
-
UPDATE / DELETE:多角色权限用 AND 连接(交集,更安全)
常见错误
// 1. 注解放在 Controller 层(无效,拦截器在 Mapper 执行前生效) @Controller public class OrderController { @DataPermission({...}) // 无效!应在 Service 或 Mapper 上 public Result<?> list() { } }
// 2. 表别名不匹配 @DataColumn(key = "deptName", value = "user.dept_id") // SQL 中别名是 u // 应为 value = "u.dept_id"
// 3. 在权限服务内部调用带权限的方法(死循环) public String getDeptAndChild(Long deptId) { deptService.list(wrapper); // 如果 list 也带 @DataPermission -> 死循环 // 应直接用 Mapper 或 ignore() 包装 }
// 4. 忘记在表中添加部门/创建人字段 // 没有 dept_id / created_by 字段,权限 SQL 会报错
// 5. 超级管理员测试数据权限 // 超管通常跳过权限过滤,应使用普通用户账号测试
// 6. @DataPermission 注解为空 @DataPermission // 空注解,无 @DataColumn 映射,不会生效
问题排查
检查项 可能原因 解决方案
超级管理员? 超管自动跳过权限 用普通用户测试
角色数据范围? 范围为"全部数据" 修改角色数据权限配置
注解位置? 不在 Service / Mapper 层 移动注解到正确位置
表别名? value 别名与 SQL 不一致 检查并修正别名
Unknown column? 表中没有该字段 检查数据库表结构
调试:开启 SQL 日志查看拼接结果
MyBatis SQL 日志
logging: level: [你的Mapper包路径]: debug