Skip to content

数据权限

youlai-nest 支持数据权限控制,实现方案与 youlai-boot 完全对齐。

基于 装饰器 + Guard + 拦截器 + QueryBuilder 插件 的组合方案:

  • @DataPermission 装饰器:标记需要数据权限过滤的方法,支持自定义表别名和字段名
  • DataScopeGuard:从请求中解析当前用户的多角色数据权限信息,写入 RequestContext
  • DataPermissionInterceptor:从装饰器获取配置,传递给 QueryBuilder 插件
  • QueryBuilder 插件:在 getMany/getOne 时自动注入 SQL 条件,支持多角色并集策略

与 youlai-boot 对齐

特性youlai-bootyoulai-nest
注解/装饰器@DataPermission@DataPermission
多角色策略并集(OR)策略并集(OR)策略
自定义表别名deptAlias, userAliasdeptAlias, userAlias
自定义字段名deptIdColumnName, userIdColumnNamedeptIdColumnName, userIdColumnName
跳过数据权限无注解时跳过@SkipDataPermission()
超级管理员豁免ROOT 角色跳过ROOT 角色跳过

整体架构图

执行时序图

数据范围枚举 DataScopeEnum

位置:src/common/enums/data-scope.enum.ts

ts
export enum DataScopeEnum {
  ALL = 1,           // 所有数据 - 最高权限
  DEPT_AND_SUB = 2,  // 部门及子部门数据
  DEPT = 3,          // 本部门数据
  SELF = 4,          // 本人数据
  CUSTOM = 5,        // 自定义部门数据
}

注意:枚举值越小,权限范围越大。这便于比较取最大权限。

核心组件

1. @DataPermission 装饰器

位置:src/common/decorators/data-permission.decorator.ts

ts
export interface DataPermissionConfig {
  /** 部门表别名(多表关联时使用) */
  deptAlias?: string;
  /** 部门ID字段名,默认 "dept_id" */
  deptIdColumnName?: string;
  /** 用户表别名(多表关联时使用) */
  userAlias?: string;
  /** 用户ID字段名,默认 "create_by" */
  userIdColumnName?: string;
}

export function DataPermission(config?: DataPermissionConfig): MethodDecorator & ClassDecorator;
export function SkipDataPermission(): MethodDecorator;

2. RoleDataScope 模型

位置:src/common/models/role-data-scope.model.ts

支持多角色数据权限,每个角色独立维护数据权限信息:

ts
export class RoleDataScope {
  roleCode: string;        // 角色编码
  dataScope: number;       // 数据权限范围值 (1-5)
  customDeptIds?: number[]; // 自定义部门ID列表

  // 工厂方法
  static all(roleCode: string): RoleDataScope;
  static deptAndSub(roleCode: string): RoleDataScope;
  static dept(roleCode: string): RoleDataScope;
  static self(roleCode: string): RoleDataScope;
  static custom(roleCode: string, deptIds: number[]): RoleDataScope;
}

3. RequestContext 上下文

位置:src/common/context/request-context.ts

使用 AsyncLocalStorage 实现请求级别的上下文隔离:

ts
export interface CurrentUserContext {
  userId: string;
  deptId: string | null;
  deptTreePath: string | null;
  dataScopes: RoleDataScope[];  // 多角色数据权限列表
  roles?: string[];
  perms?: string[];
  isRoot?: boolean;
}

export class RequestContext {
  static getUserId(): string | null;
  static getDeptId(): string | null;
  static getDataScopes(): RoleDataScope[];
  static isRoot(): boolean;
  static setDataPermissionConfig(config: DataPermissionConfig | null): void;
  static getDataPermissionConfig(): DataPermissionConfig | null;
}

4. DataPermissionPlugin 插件

位置:src/common/plugins/data-permission.plugin.ts

核心逻辑:多角色并集策略

ts
// 构建多角色并集表达式
// 用户有多个角色时,使用 OR 连接各角色的数据权限条件
private static buildUnionExpression(
  qb: SelectQueryBuilder<T>,
  config: DataPermissionConfig,
  dataScopes: RoleDataScope[]
): { expression: string; parameters: Record<string, any> } | null {
  const expressions: string[] = [];
  
  for (const dataScope of dataScopes) {
    const result = this.buildRoleDataScopeExpression(qb, config, dataScope);
    if (result) {
      expressions.push(result.expression);
    }
  }
  
  // 使用 OR 连接所有表达式
  const unionExpression = expressions.map(expr => `(${expr})`).join(" OR ");
  return { expression: `(${unionExpression})`, parameters };
}

各权限类型生成的 SQL 条件

权限类型生成的 SQL 条件
ALL无过滤条件
DEPT_AND_SUBdept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
DEPTdept_id = ?
SELFcreate_by = ?
CUSTOMdept_id IN (?, ?, ?)

使用示例

基本使用

ts
import { DataPermission } from "../../common/decorators/data-permission.decorator";

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>
  ) {}

  // 使用默认配置:dept_id, create_by
  @DataPermission()
  async getUserList(query: UserQuery): Promise<User[]> {
    const qb = this.userRepository.createQueryBuilder("user");
    return qb.getMany();  // 数据权限条件自动注入
  }

  // 自定义表别名
  @DataPermission({ deptAlias: "u", userAlias: "u" })
  async getUserPage(page: number, pageSize: number) {
    const qb = this.userRepository
      .createQueryBuilder("u")
      .skip((page - 1) * pageSize)
      .take(pageSize);
    return qb.getManyAndCount();
  }

  // 自定义字段名
  @DataPermission({
    deptAlias: "task",
    deptIdColumnName: "org_id",
    userAlias: "task",
    userIdColumnName: "creator"
  })
  async getTaskList() {
    const qb = this.taskRepository.createQueryBuilder("task");
    return qb.getMany();
  }

  // 跳过数据权限过滤
  @SkipDataPermission()
  async getAllUsers() {
    return this.userRepository.find();
  }
}

多表关联查询

ts
@DataPermission({ deptAlias: "u", userAlias: "u" })
async getUserWithDept() {
  const qb = this.userRepository
    .createQueryBuilder("u")
    .leftJoin("sys_dept", "d", "u.dept_id = d.id")
    .select(["u.id", "u.username", "d.name as deptName"]);
  
  // 数据权限条件注入到 u 表
  // 生成: WHERE (u.dept_id = ? OR u.create_by = ?)
  return qb.getRawMany();
}

多角色数据权限

用户拥有多个角色时,采用并集策略(OR 连接)

ts
// 用户拥有两个角色:
// - 部门管理员: dataScope = 3 (本部门)
// - 普通用户: dataScope = 4 (本人)

// 生成的 SQL 条件:
WHERE (u.dept_id = 10 OR u.create_by = 100)

// 用户可以看到本部门的数据 OR 自己创建的数据

RoleService.getRoleDataScopes

获取用户所有角色的数据权限信息:

ts
async getRoleDataScopes(roleCodes: string[]): Promise<RoleDataScope[]> {
  // 1. 查询角色的数据权限
  const roles = await this.roleRepository
    .createQueryBuilder("role")
    .select(["role.code", "role.dataScope"])
    .where("role.code IN (:...roleCodes)", { roleCodes })
    .getMany();

  // 2. 获取自定义权限角色的部门ID
  // 查询 sys_role_dept 表

  // 3. 构建 RoleDataScope 列表
  return roles.map(role => new RoleDataScope(role.code, role.dataScope, customDeptIds));
}

数据库设计

sys_role 表

sql
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(32) NOT NULL COMMENT '角色编码',
  `data_scope` tinyint COMMENT '数据权限(1-所有 2-部门及子部门 3-本部门 4-本人 5-自定义)',
  PRIMARY KEY (`id`)
);

sys_role_dept 表(自定义权限)

sql
CREATE TABLE `sys_role_dept` (
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `dept_id` bigint NOT NULL COMMENT '部门ID',
  UNIQUE INDEX `uk_roleid_deptid`(`role_id`, `dept_id`)
);

配置与初始化

AppModule 中注册:

ts
import { DataPermissionInterceptor } from "./common/interceptors/data-permission.interceptor";
import { initDataPermissionPlugin } from "./common/plugins/data-permission.plugin";

@Module({
  providers: [
    // 数据权限全局守卫
    { provide: APP_GUARD, useClass: DataScopeGuard },
    // 数据权限拦截器
    { provide: APP_INTERCEPTOR, useClass: DataPermissionInterceptor },
  ],
})
export class AppModule implements OnModuleInit {
  onModuleInit() {
    // 初始化数据权限插件
    initDataPermissionPlugin();
  }
}

注意事项

  1. 只有 QueryBuilder 查询生效Repository.find() 等方法不会自动应用数据权限
  2. 必须使用 @DataPermission 装饰器:未标记的方法不会应用数据权限
  3. 超级管理员自动跳过:ROOT 角色不受数据权限限制
  4. 使用 @SkipDataPermission 跳过:某些场景需要跳过时可使用此装饰器
  5. 并集策略:多角色时权限取并集,用户能看到所有角色权限范围内的数据

基于 MIT 许可发布