数据权限
youlai-nest 支持数据权限控制,实现方案与 youlai-boot 完全对齐。
基于 装饰器 + Guard + 拦截器 + QueryBuilder 插件 的组合方案:
- @DataPermission 装饰器:标记需要数据权限过滤的方法,支持自定义表别名和字段名
- DataScopeGuard:从请求中解析当前用户的多角色数据权限信息,写入
RequestContext - DataPermissionInterceptor:从装饰器获取配置,传递给 QueryBuilder 插件
- QueryBuilder 插件:在
getMany/getOne时自动注入 SQL 条件,支持多角色并集策略
与 youlai-boot 对齐
| 特性 | youlai-boot | youlai-nest |
|---|---|---|
| 注解/装饰器 | @DataPermission | @DataPermission |
| 多角色策略 | 并集(OR)策略 | 并集(OR)策略 |
| 自定义表别名 | deptAlias, userAlias | deptAlias, userAlias |
| 自定义字段名 | deptIdColumnName, userIdColumnName | deptIdColumnName, 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_SUB | dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path)) |
| DEPT | dept_id = ? |
| SELF | create_by = ? |
| CUSTOM | dept_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();
}
}注意事项
- 只有 QueryBuilder 查询生效:
Repository.find()等方法不会自动应用数据权限 - 必须使用 @DataPermission 装饰器:未标记的方法不会应用数据权限
- 超级管理员自动跳过:ROOT 角色不受数据权限限制
- 使用 @SkipDataPermission 跳过:某些场景需要跳过时可使用此装饰器
- 并集策略:多角色时权限取并集,用户能看到所有角色权限范围内的数据
