首页 / NestJS 入门教程 / 守卫 Guard

NestJS 入门教程

守卫 Guard

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

NestJSGuard守卫认证授权CanActivateExecutionContext

本节目标:搞懂守卫是什么、什么时候执行、怎么写,学完你能给任意路由加上”门禁卡”机制。

守卫是什么

想象一下小区的门禁系统。保安不关心你从哪来、穿什么衣服,他只检查一件事——你有没有门禁卡。有卡放行,没卡拦住。

守卫干的就是这个活。

在 NestJS 里,守卫是一个用 @Injectable() 装饰的类,它实现了 CanActivate 接口。它的职责非常单一:判断当前请求该不该放行

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // 返回 true 放行,返回 false 拒绝
    return true;
  }
}

canActivate 方法的返回值决定了请求的命运:

  • 返回 true(或 Promise<true>Observable<true>)→ 请求继续,进入控制器
  • 返回 false → NestJS 直接抛出 ForbiddenException,返回 403
Note

canActivate 的返回值支持三种形式:同步 boolean、异步 Promise<boolean>、响应式 Observable<boolean>。NestJS 都会自动处理,你不用手动转换。

守卫在请求生命周期中的位置

这一点很关键——守卫到底在哪一步执行?

请求进入 → 中间件 → 守卫 → 拦截器(前) → 管道 → 控制器 → 拦截器(后) → 响应

守卫在中间件之后、拦截器和管道之前执行。

很多人会问:中间件不也能做认证吗,为什么还要搞个守卫?

区别在于”信息量”。中间件是”盲”的——它不知道接下来会执行哪个控制器、哪个方法。而守卫能拿到 ExecutionContext,它清楚地知道请求要交给谁处理。

特性中间件守卫
执行时机路由匹配之前路由匹配之后
能否访问 ExecutionContext不能
典型用途日志、请求改写认证、授权
抛异常需要手动处理框架自动处理
Tip

简单记:中间件适合做”通用处理”(所有请求都过一遍),守卫适合做”权限判断”(看路由决定放不放行)。

ExecutionContext 是什么

canActivate 方法接收一个参数:ExecutionContext。它是守卫的”情报中心”,告诉你当前请求的完整上下文。

interface ExecutionContext extends ArgumentsHost {
  getClass<T>(): Type<T>;     // 获取当前控制器类
  getHandler(): Function;     // 获取当前处理方法
}

通过 ExecutionContext,你可以拿到:

  • 当前的 HTTP 请求/响应对象
  • 当前正在执行的控制器类
  • 当前正在执行的方法引用
  • 方法的参数列表
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // 拿到 HTTP 请求对象
    const request = context.switchToHttp().getRequest();

    // 拿到控制器类
    const controllerClass = context.getClass();

    // 拿到处理方法
    const handler = context.getHandler();

    // 你的判断逻辑...
    return !!request.headers.authorization;
  }
}
Note

switchToHttp() 是 HTTP 场景下最常用的方法。如果你的应用还涉及 WebSocket 或 GraphQL,可以用 switchToWs()switchToRpc() 来切换上下文。

写一个认证守卫

来看一个实际场景:检查请求头里有没有合法的 Token。

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const token = this.extractToken(request);

    if (!token) {
      throw new UnauthorizedException('缺少认证令牌');
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      // 把用户信息挂到 request 上,后续控制器可以直接用
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException('令牌无效或已过期');
    }

    return true;
  }

  private extractToken(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

这段代码做了三件事:

  1. 从请求头提取 Bearer Token
  2. 用 JwtService 验证 Token 合法性
  3. 验证通过后,把解析出的用户信息挂到 request.user
Tip

把用户信息挂到 request.user 是 Node.js 社区的惯例。后续的控制器、拦截器都可以通过 @Request() req 拿到 req.user

守卫的三种绑定方式

守卫和管道、异常过滤器一样,支持三个级别的绑定。

方法级别

只保护某一个路由:

@Controller('users')
export class UsersController {
  @Get('profile')
  @UseGuards(JwtAuthGuard)
  getProfile(@Request() req) {
    return req.user;
  }

  @Get('public-info')
  getPublicInfo() {
    return '不需要认证';
  }
}

控制器级别

保护整个控制器的所有路由:

@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  @Get()
  findAll() {}   // 需要认证

  @Get(':id')
  findOne() {}   // 也需要认证
}

全局级别

所有路由都受保护:

// 方式一:在 main.ts 中注册
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new JwtAuthGuard());
Warning

useGlobalGuards() 创建的守卫是在模块上下文之外实例化的,这意味着它无法注入依赖。如果你需要在全局守卫中使用 JwtService 之类的服务,要用方式二。

// 方式二:在模块中用 APP_GUARD 注册(推荐)
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

APP_GUARD 注册的全局守卫可以正常注入依赖,而且它本质上还是全局生效的。

Tip

这里很多人会纠结”该写在哪个模块里”。答案很简单:写在守卫定义所在的那个模块就行。

角色守卫

认证解决的是”你是谁”,角色守卫解决的是”你能不能做这件事”。

先定义一个角色枚举:

export enum Role {
  USER = 'user',
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin',
}

然后创建一个 @Roles() 装饰器,用来标记路由需要哪些角色:

import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';

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

SetMetadata 是 NestJS 提供的内置装饰器,作用是在路由处理方法上”贴标签”。守卫通过 Reflector 来读取这些标签。

接下来写角色守卫:

import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
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<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.role === role);
    if (!hasRole) {
      throw new ForbiddenException('权限不足');
    }

    return true;
  }
}

Reflector.getAllAndOverride 会同时检查方法级别和控制器级别的元数据,方法级别的优先。这是最常用的读取方式。

使用的时候,把认证守卫和角色守卫叠在一起:

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
export class AdminController {
  @Get('users')
  findAllUsers() {
    return '所有用户列表';
  }

  @Get('settings')
  @Roles(Role.SUPER_ADMIN)  // 方法级别覆盖控制器级别
  getSettings() {
    return '系统设置(仅超级管理员)';
  }
}
Tip

多个守卫按数组顺序依次执行。JwtAuthGuard 先跑,确认用户身份;RolesGuard 后跑,检查角色权限。顺序不能反。

公开路由

全局守卫把所有路由都保护了,但登录、注册这些接口显然不应该需要认证。怎么办?

写一个 @Public() 装饰器来标记”这个路由是公开的”:

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

然后在认证守卫里检查这个标记:

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private reflector: Reflector,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    // 检查是否是公开路由
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;  // 公开路由,直接放行
    }

    // 正常的 Token 验证逻辑...
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    // ...
    return true;
  }
}

用起来就很简单了:

@Controller('auth')
export class AuthController {
  @Post('login')
  @Public()
  login() {
    return '登录接口,无需认证';
  }

  @Get('profile')
  getProfile() {
    return '需要认证才能访问';
  }
}

守卫组合

实际项目中,多个守卫组合使用是很常见的场景:

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
export class AdminController {
  @Get('dashboard')
  @Roles(Role.ADMIN)
  @RequirePermissions('dashboard:read')
  getDashboard() {
    return '管理后台仪表盘';
  }
}

三个守卫各司其职:

  • JwtAuthGuard:验证 Token,确认用户身份
  • RolesGuard:检查用户角色是否匹配
  • PermissionsGuard:检查用户是否有具体操作权限
Note

每个守卫只负责一件事,这就是”单一职责原则”。不要把认证、角色、权限全塞进一个守卫里,那样代码会变得很难维护。

守卫的最佳实践

1. 单一职责

一个守卫只做一种检查。认证归认证守卫,角色归角色守卫,权限归权限守卫。

2. 用装饰器传递配置

不要在守卫里硬编码路由和角色的对应关系。用 @Roles()@Public() 这类装饰器把配置”贴”在路由上,守卫只负责读取和判断。

3. 给出清晰的错误信息

throw new ForbiddenException({
  message: '权限不足',
  requiredRole: Role.ADMIN,
  currentRole: user.role,
});

别只返回一个 false 就完事了。给前端足够的信息,调试的时候你会感谢自己。

4. 优先用类而非实例绑定守卫

// 推荐:传类,框架负责实例化,支持依赖注入
@UseGuards(AuthGuard)

// 不推荐:传实例,无法注入依赖
@UseGuards(new AuthGuard())

小结

守卫的核心就一句话:在路由匹配之后、业务逻辑之前,决定请求能不能通过

关键知识点回顾:

  • 守卫实现 CanActivate 接口,核心方法是 canActivate()
  • ExecutionContext 提供控制器类、处理方法、请求对象等信息
  • 支持方法级、控制器级、全局级三种绑定方式
  • 全局守卫用 APP_GUARD 注册才能注入依赖
  • Reflector + 自定义装饰器实现灵活的角色/权限控制
  • 多个守卫按顺序执行,各自保持单一职责

下一章我们聊聊拦截器,看看怎么在请求执行过程中插入自定义逻辑。