Skip to content

NestJS 守卫

基础语法

守卫是被 @Injectable() 装饰的类,必须实现 CanActivate 接口(唯一方法 canActivate),其核心职责:

  • 接收请求上下文,判断是否允许访问目标路由;
  • 返回 boolean
    • true:允许访问,请求继续向下执行(管道→拦截器→路由处理器);
    • false:拒绝访问,Nest 自动抛出 ForbiddenException(403 状态码)。
ts
// src/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable() // 必须标记为可注入,支持依赖注入
export class RolesGuard implements CanActivate {
  // 核心方法:决定是否放行请求
  canActivate(context: ExecutionContext): boolean {
    // 1. 获取HTTP请求上下文
    const request = context.switchToHttp().getRequest();
    // 2. 模拟授权逻辑:检查用户是否登录
    const isLoggedIn = !!request.user;
    return isLoggedIn;
  }
}

参数说明

ExecutionContext:上下文对象,封装了当前请求的所有信息(HTTP/RPC/WebSocket 通用)

shell
方法            作用                                       示例(HTTP 上下文)
getClass()     获取当前控制器类                              context.getClass() → AppController
getHandler()   获取当前路由处理方法                           context.getHandler() → getUser 方法
switchToHttp() 切换到 HTTP 上下文(返回 HttpArgumentsHost)   const req = context.switchToHttp().getRequest()
switchToRpc()  切换到 RPC 上下文(微服务)                     -
switchToWs()   切换到 WebSocket 上下文                       -
getType()      获取上下文类型(http/rpc/ws)                  context.getType() → http
getArgs()      获取路由处理器的参数数组                         -

多个守卫的执行顺序

@UseGuards() 可传入多个守卫,执行顺序为从左到右,任一守卫返回 false 则终止请求

ts
// 先认证(AuthGuard),再鉴权(RolesGuard)
@Get('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
adminOnly() {
  return '仅管理员可访问';
}

异步守卫

canActivate 支持返回 Promise,适用于需要异步操作(如数据库查询)的授权逻辑

ts
// src/guards/permission.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionService } from '../services/permission.service';

@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly permissionService: PermissionService // 注入权限服务
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 1. 读取路由需要的权限
    const requiredPerms = this.reflector.get<string[]>('permissions', context.getHandler());
    if (!requiredPerms) return true;

    // 2. 获取当前用户
    const { user } = context.switchToHttp().getRequest();

    // 3. 异步查询用户权限(数据库操作)
    const userPerms = await this.permissionService.getUserPermissions(user.id);

    // 4. 校验权限
    return requiredPerms.every((perm) => userPerms.includes(perm));
  }
}

自定义异常

默认返回 403 Forbidden,可主动抛出自定义异常

ts
canActivate(context: ExecutionContext): boolean {
  const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
  const { user } = context.switchToHttp().getRequest();

  if (!requiredRoles.includes(user.role)) {
    // 自定义异常消息和状态码
    throw new ForbiddenException(`需要角色:${requiredRoles.join(', ')},当前角色:${user.role}`);
    // 或通用HTTP异常
    // throw new HttpException('无权访问', HttpStatus.FORBIDDEN);
  }
  return true;
}

基础守卫

IP 白名单守卫

  1. 定义守卫
ts
// src/guards/ip-whitelist.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';

@Injectable()
export class IpWhitelistGuard implements CanActivate {
  // 白名单 IP(可从配置文件注入)
  private readonly whitelistIps = ['127.0.0.1', '192.168.1.100'];

  canActivate(context: ExecutionContext): boolean {
    // 解析 HTTP 请求上下文
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest();
    // 获取客户端 IP(不同环境可能需要调整,如 X-Forwarded-For)
    const clientIp = request.ip || request.ips[0] || request.connection.remoteAddress;

    // 校验 IP
    const isAllowed = this.whitelistIps.includes(clientIp);
    if (!isAllowed) {
      // 自定义异常(替代默认的 403)
      throw new ForbiddenException(`IP ${clientIp} 无访问权限`);
    }
    return isAllowed;
  }
}
  1. 使用守卫
ts
// src/app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { IpWhitelistGuard } from './guards/ip-whitelist.guard';

@Controller()
export class AppController {
  // 路由级别守卫
  @Get('protected')
  @UseGuards(IpWhitelistGuard)
  getProtectedData() {
    return { message: '仅白名单 IP 可访问' };
  }
}

守卫结合元数据

守卫常需结合路由元数据(如 “该路由需要管理员角色”),Nest 提供 Reflector 类用于读取 / 设置元数据,

  1. 自定义元数据装饰器:避免直接使用 @SetMetadata,封装为语义化装饰器
ts
// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

// 定义元数据 key 和装饰器
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
  1. 实现角色守卫:在守卫中读取元数据
ts
// Reflector 核心方法
// 方法                 作用
// getAllAndOverride 优先取路由级别元数据,无则取控制器级别
// getAllAndMerge     合并路由 + 控制器级别元数据(如数组拼接)
// get               读取单个目标(如仅路由)的元数据

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

@Injectable()
export class RolesGuard implements CanActivate {
  // 注入 Reflector 用于读取元数据
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. 读取路由上的 @Roles 元数据(优先级:路由 → 控制器 → 全局)
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(), // 路由方法的元数据
      context.getClass(), // 控制器的元数据
    ]);

    // 2. 无角色要求 → 直接放行
    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

    // 3. 解析请求中的用户(假设 JWT 中间件已将用户信息挂载到 req.user)
    const request = context.switchToHttp().getRequest();
    const user = request.user; // 格式:{ id: 1, roles: ['admin'] }

    // 4. 校验用户角色
    const hasRequiredRole = user?.roles?.some((role) => requiredRoles.includes(role));
    if (!hasRequiredRole) {
      throw new ForbiddenException(
        `需要角色:${requiredRoles.join(', ')},当前角色:${user?.roles?.join(', ') || ''}`
      );
    }

    return true;
  }
}
  1. 使用角色守卫
ts
// src/app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './decorators/roles.decorator';

@Controller()
export class AppController {
  // 需 admin 角色才能访问
  @Get('admin')
  @UseGuards(RolesGuard)
  @Roles('admin')
  getAdminData() {
    return { message: '管理员专属数据' };
  }

  // 需 editor 或 admin 角色
  @Get('editor')
  @UseGuards(RolesGuard)
  @Roles('editor', 'admin')
  getEditorData() {
    return { message: '编辑/管理员专属数据' };
  }
}