首页 / NestJS 入门教程 / 权限控制

NestJS 入门教程

权限控制

本教程共 47 篇 · 第 22 篇 · 更新于 2026-08-09 · 约 12 分钟阅读

NestJS权限控制RBAC守卫CASL角色装饰器授权

本节目标:学会在 NestJS 中实现权限控制,包括 RBAC 角色模型、细粒度权限检查和 CASL 权限库集成,让你的 API 真正做到”谁该干啥就干啥”。

上一章我们用 JWT 解决了”你是谁”的问题。这一章要解决的是”你能干啥”。

打个比方:门禁卡让你进了大楼(认证),但不同楼层的门禁权限不一样——你能进办公区,不一定能进机房。这就是权限控制(Authorization)干的事。

权限控制的几种模型

在动手写代码之前,先了解常见的权限模型,心里有个数:

模型全称核心思路适合场景
RBAC基于角色的访问控制用户 -> 角色 -> 权限大多数后台管理系统
ABAC基于属性的访问控制根据用户/资源属性动态判断复杂业务规则
ACL访问控制列表逐个列出谁对什么有什么权限简单的小项目

实际项目中,RBAC 用得最多。我们就从它开始。

RBAC:基于角色的权限控制

第一步:定义角色和权限枚举

先把角色和权限用枚举定义清楚。这就像给公司定组织架构——先有哪些角色,每个角色能干什么。

// enums/role.enum.ts
export enum Role {
  USER = 'user',
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin',
}

// enums/permission.enum.ts
export enum Permission {
  USER_READ = 'user:read',
  USER_CREATE = 'user:create',
  USER_UPDATE = 'user:update',
  USER_DELETE = 'user:delete',
  POST_READ = 'post:read',
  POST_CREATE = 'post:create',
  POST_UPDATE = 'post:update',
  POST_DELETE = 'post:delete',
}
Tip

权限用 资源:操作 的格式命名,比如 user:createpost:delete。这种命名方式一目了然,后期维护也方便。

第二步:建立角色-权限映射

接下来定义每个角色拥有哪些权限。你可以把它理解成一张”权限表”。

// config/role-permissions.ts
import { Role } from '../enums/role.enum';
import { Permission } from '../enums/permission.enum';

export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  [Role.USER]: [
    Permission.USER_READ,
    Permission.POST_READ,
    Permission.POST_CREATE,
  ],
  [Role.ADMIN]: [
    Permission.USER_READ,
    Permission.USER_CREATE,
    Permission.USER_UPDATE,
    Permission.POST_READ,
    Permission.POST_CREATE,
    Permission.POST_UPDATE,
    Permission.POST_DELETE,
  ],
  [Role.SUPER_ADMIN]: Object.values(Permission), // 超级管理员拥有所有权限
};

第三步:创建装饰器

回忆一下第 19 章学的自定义装饰器。我们需要两个装饰器:一个标记路由需要的角色,一个标记需要的权限。

// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// decorators/permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Permission } from '../enums/permission.enum';

export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: Permission[]) =>
  SetMetadata(PERMISSIONS_KEY, permissions);

用法和第 19 章一模一样——@SetMetadata 把信息”贴”到路由处理函数上,守卫再来”撕下来”读取。

第四步:编写角色守卫

角色守卫的逻辑很简单:取出路由需要的角色,看看当前用户有没有其中之一。

// guards/roles.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 读取路由上标记的角色要求
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );

    // 没标记角色?说明不需要权限控制,直接放行
    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();

    if (!user) {
      throw new ForbiddenException('用户未认证');
    }

    // 用户拥有的角色中,只要有一个匹配就行
    const hasRole = requiredRoles.some((role) => user.roles?.includes(role));

    if (!hasRole) {
      throw new ForbiddenException('角色权限不足');
    }

    return true;
  }
}
Note

reflector.getAllAndOverride 会先查方法上的元数据,再查类上的。如果方法上标了 @Roles(Role.Admin),类上标了 @Roles(Role.User),最终取到的是 [Role.Admin]。如果你想取两者的合集,可以用 reflector.getAllAndMerge

第五步:编写权限守卫

权限守卫比角色守卫更进一步——它检查的不是”你是不是某个角色”,而是”你有没有某个具体权限”。

// guards/permissions.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Permission } from '../enums/permission.enum';
import { ROLE_PERMISSIONS } from '../config/role-permissions';
import { Role } from '../enums/role.enum';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
      PERMISSIONS_KEY,
      [context.getHandler(), context.getClass()],
    );

    if (!requiredPermissions) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();

    if (!user) {
      throw new ForbiddenException('用户未认证');
    }

    // 根据用户角色,查出他拥有的所有权限
    const userPermissions = this.getUserPermissions(user.role);

    // 必须拥有全部所需权限才能通过
    const hasAllPermissions = requiredPermissions.every((permission) =>
      userPermissions.includes(permission),
    );

    if (!hasAllPermissions) {
      throw new ForbiddenException('权限不足');
    }

    return true;
  }

  private getUserPermissions(role: Role): Permission[] {
    return ROLE_PERMISSIONS[role] || [];
  }
}

在控制器中使用

现在把守卫和装饰器配合起来用。假设我们有一个用户管理模块:

// users/users.controller.ts
import { Controller, Get, Post, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { PermissionsGuard } from './guards/permissions.guard';
import { Roles } from './decorators/roles.decorator';
import { RequirePermissions } from './decorators/permissions.decorator';
import { Role, Permission } from './enums';

@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
export class UsersController {
  @Get()
  @Roles(Role.ADMIN, Role.SUPER_ADMIN)
  @RequirePermissions(Permission.USER_READ)
  findAll() {
    return '获取用户列表';
  }

  @Post()
  @Roles(Role.ADMIN, Role.SUPER_ADMIN)
  @RequirePermissions(Permission.USER_CREATE)
  create() {
    return '创建用户';
  }

  @Delete(':id')
  @Roles(Role.SUPER_ADMIN)
  @RequirePermissions(Permission.USER_DELETE)
  remove() {
    return '删除用户';
  }
}

请求到达时,守卫按顺序执行:

  1. JwtAuthGuard —— 验证 JWT,确认用户身份
  2. RolesGuard —— 检查用户角色是否满足要求
  3. PermissionsGuard —— 检查用户是否拥有具体权限

三层都通过了,才执行控制器方法。

动态权限:从数据库读取

前面我们把角色-权限映射写死在代码里。小项目够用了,但实际项目中权限经常需要动态管理——管理员在后台界面上勾选分配权限,数据存在数据库里。

数据库实体设计

典型的 RBAC 数据库模型是这样的:用户和角色是多对多,角色和权限也是多对多。

// entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Role, (role) => role.users)
  @JoinTable()
  roles: Role[];
}
// entities/role.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Permission } from './permission.entity';
import { User } from './user.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;

  @ManyToMany(() => Permission, (permission) => permission.roles)
  @JoinTable()
  permissions: Permission[];

  @ManyToMany(() => User, (user) => user.roles)
  users: User[];
}
// entities/permission.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Role } from './role.entity';

@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;       // 例如 'user:create'

  @Column()
  resource: string;   // 资源名,例如 'user'

  @Column()
  action: string;     // 操作,例如 'create'

  @ManyToMany(() => Role, (role) => role.permissions)
  roles: Role[];
}

权限服务

写一个服务来查询用户的权限列表:

// services/permission.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

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

  async getUserPermissions(userId: number): Promise<string[]> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      relations: ['roles', 'roles.permissions'],
    });

    if (!user) return [];

    // 用 Set 去重,因为一个用户可能有多个角色,角色之间可能有重复权限
    const permissions = new Set<string>();
    user.roles.forEach((role) => {
      role.permissions.forEach((permission) => {
        permissions.add(permission.name);
      });
    });

    return Array.from(permissions);
  }

  async hasPermission(userId: number, permission: string): Promise<boolean> {
    const permissions = await this.getUserPermissions(userId);
    return permissions.includes(permission);
  }
}

动态权限守卫

把前面的静态权限守卫改成从数据库读取:

// guards/dynamic-permissions.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionService } from '../services/permission.service';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';

@Injectable()
export class DynamicPermissionsGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private permissionService: PermissionService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      PERMISSIONS_KEY,
      [context.getHandler(), context.getClass()],
    );

    if (!requiredPermissions) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();

    if (!user) {
      throw new ForbiddenException('用户未认证');
    }

    // 从数据库查询用户权限
    const userPermissions = await this.permissionService.getUserPermissions(
      user.userId,
    );

    const hasAllPermissions = requiredPermissions.every((permission) =>
      userPermissions.includes(permission),
    );

    if (!hasAllPermissions) {
      throw new ForbiddenException('权限不足');
    }

    return true;
  }
}
Tip

动态权限每次请求都查数据库,性能开销不小。生产环境中建议把用户权限缓存到 Redis,用户登录时写入缓存,权限变更时清除缓存。

资源级权限

RBAC 解决的是”能不能做某类操作”。但有时候我们需要更细粒度的控制——“能不能操作这个具体的资源”。

举个例子:用户可以编辑自己的文章,但不能编辑别人的。这就是资源级权限。

// guards/resource-owner.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  NotFoundException,
} from '@nestjs/common';
import { PostsService } from '../posts/posts.service';

@Injectable()
export class ResourceOwnerGuard implements CanActivate {
  constructor(private postsService: PostsService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const { user, params } = request;
    const postId = parseInt(params.id);

    const post = await this.postsService.findOne(postId);

    if (!post) {
      throw new NotFoundException('文章不存在');
    }

    // 管理员可以操作任何文章,普通用户只能操作自己的
    if (post.authorId !== user.userId && user.role !== 'admin') {
      throw new ForbiddenException('你不是这篇文章的作者');
    }

    return true;
  }
}

在控制器中使用:

@Put(':id')
@UseGuards(JwtAuthGuard, ResourceOwnerGuard)
update(
  @Param('id') id: string,
  @Body() updatePostDto: UpdatePostDto,
) {
  return this.postsService.update(+id, updatePostDto);
}
Note

资源级权限检查通常需要在守卫中查询数据库,所以 canActivate 要声明为 async,返回 Promise<boolean>

ABAC:基于属性的访问控制

有些业务规则用角色描述不了。比如:

  • 用户只能编辑自己创建的文章
  • 已发布的文章不能被删除
  • 部门经理可以审批本部门的报销单

这些规则跟角色关系不大,主要看”用户属性”和”资源属性”之间的关系。这就是 ABAC 的思路。

定义策略类型

// types/policy.types.ts
export type PolicyHandler = (
  user: any,
  resource: any,
) => boolean | Promise<boolean>;

创建策略装饰器

// decorators/policy.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { PolicyHandler } from '../types/policy.types';

export const POLICY_KEY = 'policy';
export const Policy = (handler: PolicyHandler) =>
  SetMetadata(POLICY_KEY, handler);

创建策略守卫

// guards/policy.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PolicyHandler } from '../types/policy.types';
import { POLICY_KEY } from '../decorators/policy.decorator';

@Injectable()
export class PolicyGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandler = this.reflector.getAllAndOverride<PolicyHandler>(
      POLICY_KEY,
      [context.getHandler(), context.getClass()],
    );

    if (!policyHandler) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const { user, params, body } = request;

    if (!user) {
      throw new ForbiddenException('用户未认证');
    }

    const resource = { ...params, ...body };
    const canAccess = await policyHandler(user, resource);

    if (!canAccess) {
      throw new ForbiddenException('不符合访问策略');
    }

    return true;
  }
}

使用 ABAC

定义一个策略函数,然后贴到路由上:

// 策略:只有管理员或文章作者才能编辑
const canUpdatePost: PolicyHandler = (user, resource) => {
  return user.role === 'admin' || Number(resource.authorId) === user.userId;
};

@Put(':id')
@UseGuards(JwtAuthGuard, PolicyGuard)
@Policy(canUpdatePost)
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
  return this.postsService.update(+id, updatePostDto);
}

ABAC 的好处是灵活——策略函数想写多复杂就写多复杂。坏处是容易散落各处,不好统一管理。

CASL:更优雅的权限方案

当权限规则越来越多,散落各处的策略函数会变成噩梦。CASL 就是来解决这个问题的——它把权限规则集中定义,提供统一的查询接口。

安装

npm install @casl/ability

定义能力工厂

CASL 的核心概念是”能力”(Ability)。一个能力描述了”谁对什么能做什么”。

// casl/casl-ability.factory.ts
import { Injectable } from '@nestjs/common';
import {
  MongoAbility,
  AbilityBuilder,
  createMongoAbility,
  ExtractSubjectType,
  InferSubjects,
} from '@casl/ability';

// 定义操作类型
export enum Action {
  Manage = 'manage',  // 所有操作
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

// 定义实体类(简化版)
export class Article {
  id: number;
  isPublished: boolean;
  authorId: number;
}

type Subjects = InferSubjects<typeof Article> | 'all';
export type AppAbility = MongoAbility<[Action, Subjects]>;

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: any): AppAbility {
    const { can, cannot, build } = new AbilityBuilder(createMongoAbility);

    if (user.isAdmin) {
      // 管理员可以做任何事
      can(Action.Manage, 'all');
    } else {
      // 普通用户可以读所有文章
      can(Action.Read, Article);
      // 只能创建文章
      can(Action.Create, Article);
      // 只能更新自己的文章
      can(Action.Update, Article, { authorId: user.id });
      // 不能删除已发布的文章
      cannot(Action.Delete, Article, { isPublished: true });
    }

    return build({
      detectSubjectType: (item) =>
        item.constructor as ExtractSubjectType<Subjects>,
    });
  }
}

这段代码定义了一套完整的权限规则。来看看怎么用:

const user = { id: 1, isAdmin: false };
const ability = caslAbilityFactory.createForUser(user);

const article = new Article();
article.authorId = 1;

ability.can(Action.Read, Article);   // true —— 普通用户可以读
ability.can(Action.Create, Article); // true —— 普通用户可以创建
ability.can(Action.Update, article); // true —— 作者可以更新自己的文章
ability.can(Action.Delete, article); // true —— 未发布可以删

article.isPublished = true;
ability.can(Action.Delete, article); // false —— 已发布不能删

创建策略守卫

把 CASL 和 NestJS 守卫结合起来:

// casl/policies.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AppAbility, CaslAbilityFactory } from './casl-ability.factory';

// 策略处理器可以是函数或对象
interface IPolicyHandler {
  handle(ability: AppAbility): boolean;
}
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;

// 装饰器
export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
  SetMetadata(CHECK_POLICIES_KEY, handlers);

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private caslAbilityFactory: CaslAbilityFactory,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers =
      this.reflector.get<PolicyHandler[]>(
        CHECK_POLICIES_KEY,
        context.getHandler(),
      ) || [];

    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslAbilityFactory.createForUser(user);

    // 所有策略都必须通过
    return policyHandlers.every((handler) =>
      this.execPolicyHandler(handler, ability),
    );
  }

  private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
    if (typeof handler === 'function') {
      return handler(ability);
    }
    return handler.handle(ability);
  }
}

在控制器中使用 CASL

@Get()
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
  return this.articlesService.findAll();
}

@Delete(':id')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, Article))
remove(@Param('id') id: string) {
  return this.articlesService.remove(+id);
}

也可以用类的方式来定义策略,适合复杂逻辑:

export class DeleteArticlePolicyHandler implements IPolicyHandler {
  handle(ability: AppAbility) {
    return ability.can(Action.Delete, Article);
  }
}

@Delete(':id')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies(new DeleteArticlePolicyHandler())
remove(@Param('id') id: string) {
  return this.articlesService.remove(+id);
}

选哪种方案?

方案优点缺点适合场景
RBAC简单直观,容易理解粒度粗,不够灵活后台管理系统
ABAC非常灵活规则散落,难维护规则少且变化多
CASL集中管理,表达力强有学习成本中大型项目

小项目用 RBAC 就够了。当权限规则开始变得复杂,比如需要判断”用户只能操作自己的资源”,就该考虑 CASL 了。

Tip

实际项目中经常混合使用:RBAC 做粗粒度的角色控制,CASL 或自定义守卫做细粒度的资源控制。不用纠结”只能选一种”。

踩坑经验

1. 守卫的执行顺序很重要

多个守卫按声明顺序执行。认证守卫一定要放在权限守卫前面——你总得先知道”这是谁”,才能判断”他能干啥”。

// 正确:先认证,再鉴权
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)

// 错误:权限守卫在认证守卫前面,此时 request.user 还是空的
@UseGuards(RolesGuard, JwtAuthGuard)

2. 别忘了全局守卫

如果你的大部分接口都需要权限控制,考虑把权限守卫注册为全局守卫,然后对个别不需要权限的接口用 @Public() 装饰器跳过。

3. 缓存用户权限

动态权限每次查数据库会很慢。用户登录后把权限列表存到 JWT 的 payload 里,或者存到 Redis,能大幅减少数据库查询。

4. 权限变更要及时生效

如果用户权限改了(比如从普通用户升级为管理员),旧的 JWT 里还存着旧权限。解决方案:缩短 JWT 过期时间 + 配合 Refresh Token,或者在 Redis 中维护一个”权限版本号”。

小结

关键知识点回顾:

  • RBAC 用角色绑定权限,用户分配角色,适合大多数后台系统
  • SetMetadata + Reflector 实现细粒度的权限声明
  • CASL 提供基于属性的访问控制,适合复杂业务场景
  • 守卫的执行顺序很重要:先认证,再鉴权
  • 生产环境注意缓存用户权限、及时处理权限变更

下一章我们聊聊安全加固,把 API 的安全防线建起来。