You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 21:43:18