返回博客列表
NestJS权限系统TypeScript

NestJS RBAC 权限系统实战:用 Guard + 装饰器做细粒度鉴权

从零搭建基于角色的访问控制系统,Guard 与自定义装饰器配合,一行代码保护任意接口。

作者: ekent·发布于 2026年2月28日

企业系统里"谁能做什么"是一道永恒的难题。我们在实际项目中遇到过这样的场景:同一个后台,普通员工只能查看自己的数据,部门主管可以导出报表,管理员才能操作用户。如果把权限判断散落在每个 Controller 里,代码会越来越难维护。

NestJS 的 Guard 机制配合自定义装饰器,能让权限逻辑集中、可复用,最终只需一行声明就能保护任意接口。

什么是 RBAC

RBAC(Role-Based Access Control,基于角色的访问控制)是最常见的权限模型:

  • 用户拥有一个或多个角色(Role)
  • 角色决定用户能访问哪些资源(Resource)
  • 不直接给用户分配权限,而是通过角色间接赋予

常见角色示例:adminmanagerstaffguest

NestJS Guard 的工作原理

Guard 是 NestJS 的一个切面,执行时机在中间件之后、拦截器之前。每个请求都会经过 Guard,Guard 返回 true 放行,返回 false 或抛出异常则拒绝。

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // 验证逻辑
    return !!request.user;
  }
}

Guard 可以作用于整个 Controller 或单个路由方法,也可以通过 APP_GUARD 注册为全局守卫。

第一步:自定义 @Roles() 装饰器

我们需要一种方式,在路由上声明"哪些角色可以访问"。NestJS 的 Reflector 允许我们把元数据附加到路由上,再在 Guard 里读取。

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

用法非常简洁:

@Get('reports')
@Roles('admin', 'manager')
getReports() {
  return this.reportService.findAll();
}

第二步:实现 RolesGuard

Guard 里通过 Reflector 读取路由上附加的角色元数据,再和当前请求用户的角色做对比:

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    // 读取路由上声明的角色列表
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),  // 方法级
      context.getClass(),    // 类级(作为 fallback)
    ]);

    // 没有声明角色限制,直接放行
    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

    // 从请求中取当前用户(由 JWT Guard 提前写入)
    const { user } = context.switchToHttp().getRequest();
    if (!user) return false;

    // 判断用户角色是否满足要求
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

注意 getAllAndOverride 的优先级:方法级装饰器覆盖类级装饰器,这样可以在 Controller 层设置默认角色,再在某个方法上细化。

第三步:Guard 执行顺序

实际项目中通常有两个 Guard:先验证 Token 合法性(JWT Guard),再验证角色权限(Roles Guard)。执行顺序由注册顺序决定:

// app.module.ts
providers: [
  {
    provide: APP_GUARD,
    useClass: JwtAuthGuard,  // 先跑 JWT 验证
  },
  {
    provide: APP_GUARD,
    useClass: RolesGuard,    // 再跑角色验证
  },
],

JWT Guard 负责解析 Token、把用户信息写入 request.user;Roles Guard 再从 request.user 里读角色。两者职责分离,互不干扰。

公开路由的处理

全局注册 JWT Guard 后,登录接口自身也会被拦截。需要一个 @Public() 装饰器跳过验证:

// public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

在 JWT Guard 里检查这个标记:

canActivate(context: ExecutionContext) {
  const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
    context.getHandler(),
    context.getClass(),
  ]);
  if (isPublic) return true;
  // ... 正常 JWT 验证
}

登录接口只需加一行:

@Post('login')
@Public()
login(@Body() dto: LoginDto) { ... }

实际效果

经过以上设置,权限控制变成了纯声明式:

@Controller('users')
@UseGuards()  // 全局 Guard 自动生效,无需手动加
export class UserController {

  @Get()
  @Roles('admin')           // 仅管理员
  findAll() { ... }

  @Get(':id')
  @Roles('admin', 'manager') // 管理员或主管
  findOne() { ... }

  @Put(':id/profile')
  // 不加 @Roles,任意已登录用户可访问
  updateProfile() { ... }
}

业务代码里不再出现任何 if (user.role !== 'admin') 的判断,权限策略一目了然。

小结

层次职责
@Public()标记免验证路由
JwtAuthGuard验证 Token,填充 request.user
@Roles()声明路由所需角色
RolesGuard读取元数据,比对用户角色

这套方案在我们的企业项目中稳定运行,新增接口时权限控制只需一行声明,代码审查时也一眼看出谁有权访问。如果后续需要更细粒度的资源级权限(比如"只能操作自己的数据"),可以在 Guard 或 Service 层再加一层策略判断。


作者:ekent · ek Studio 祎坤