NestJS+PrismaORM+CASL中创建实体前校验CASL权限的问题
NestJS + Prisma + CASL 创建前权限校验解决方案
核心思路
CASL 的 ability.can() 方法不需要依赖已持久化的实体,直接传入拟创建的参数对象(比如 DTO)就能完成权限规则校验,完全适配 Prisma 直接写入数据库的特性。下面是具体实现步骤:
1. 正确定义 CASL 权限规则
在 AbilityFactory 中针对创建操作,明确参数校验规则,比如限制 unitId 匹配用户所属部门、rank 符合层级要求:
// ability.factory.ts import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability'; import { User } from '@prisma/client'; import { Role } from './role.enum'; // 定义权限主题,直接用字符串适配 Prisma 接口实体 export type Subjects = 'User' | 'all'; export type AppAbility = Ability<[string, Subjects]>; export class AbilityFactory { static createForUser(user: User) { const { can, build } = new AbilityBuilder<AppAbility>(Ability as AbilityClass<AppAbility>); if (user.role === Role.ADMIN) { can('manage', 'all'); } else { // 核心:定义创建User的参数校验规则 can('create', 'User', { unitId: user.unitId, // 必须和当前用户的unitId一致 rank: { $gt: user.role.rank } // rank必须大于当前用户角色的rank }); } return build({ // 适配字符串类型的主题 detectSubjectType: (subject) => subject as Subjects }); } }
2. 在 Service 层前置校验
调用 Prisma create 方法之前,先通过 ability.can() 校验创建参数,不通过直接抛出异常:
// user.service.ts import { Injectable, ForbiddenException } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { AbilityFactory } from './ability.factory'; import { CreateUserDto } from './dto/create-user.dto'; import { User } from '@prisma/client'; @Injectable() export class UserService { constructor( private prisma: PrismaService, private abilityFactory: AbilityFactory ) {} async create(currentUser: User, createDto: CreateUserDto) { const ability = this.abilityFactory.createForUser(currentUser); // 关键:传入拟创建的DTO参数,让CASL校验规则匹配度 if (!ability.can('create', 'User', createDto)) { throw new ForbiddenException('无权限创建该实体,或参数不符合权限要求'); } // 校验通过后再执行数据库写入 return this.prisma.user.create({ data: createDto }); } }
3. 封装 Guard 实现全局复用
如果多个 Service 都需要类似校验,可以封装成 NestJS Guard,减少重复代码:
// casl-guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AbilityFactory } from './ability.factory'; @Injectable() export class CaslGuard implements CanActivate { constructor( private reflector: Reflector, private abilityFactory: AbilityFactory ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const currentUser = request.user; // 假设已通过AuthGuard获取当前用户 const ability = this.abilityFactory.createForUser(currentUser); // 从控制器方法上获取权限元数据(需自定义@CheckPermission装饰器) const [action, subject] = this.reflector.get<[string, string]>( 'permission', context.getHandler() ); if (!action || !subject) return true; // 获取请求体中的创建参数 const dto = request.body; if (!ability.can(action, subject, dto)) { throw new ForbiddenException('权限校验失败'); } return true; } }
控制器使用示例:
// user.controller.ts import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common'; import { UserService } from './user.service'; import { CreateUserDto } from './dto/create-user.dto'; import { AuthGuard } from './auth.guard'; import { CaslGuard } from './casl-guard'; import { SetMetadata } from '@nestjs/common'; // 自定义装饰器,标记需要校验的权限 export const CheckPermission = (action: string, subject: string) => SetMetadata('permission', [action, subject]); @Controller('users') @UseGuards(AuthGuard) export class UserController { constructor(private readonly userService: UserService) {} @Post() @UseGuards(CaslGuard) @CheckPermission('create', 'User') async create(@Request() req, @Body() createDto: CreateUserDto) { return this.userService.create(req.user, createDto); } }
关键注意点
- CASL 的
can()第三个参数支持任何 Plain Object,不需要实体实例,完美适配 Prisma 的前置校验需求; - 如果 Prisma 实体是接口而非类,直接用字符串作为 Subject 即可,无需额外处理实体类型;
- 规则中的动态值(比如
user.unitId)必须从当前用户对象中获取,确保规则的动态性。
内容的提问来源于stack exchange,提问作者Anne Nonyme
相关产品推荐
相关产品推荐

