Skip to content
第 9 章 后端 ⏱ 13 分钟阅读

第 9 章:守卫 ​

学习目标 ​

  • 理解 Guard 的判断时机
  • 写 AuthGuard 和 RolesGuard
  • 掌握全局守卫和元数据反射
  • 避开 3 个 Guard 坑

一、Guard 是干嘛的 ​

Guard 用来判断请求能不能进 Controller,最常见:登录态、角色权限。

执行顺序:中间件 → Guard → Pipe → Controller。

返回值:

  • true:放行
  • false:拒绝(403)
  • 抛异常:自定义状态码

二、最简单的 Guard ​

typescript
// guards/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    return !!req.headers['authorization'];     // 有 token 就放行
  }
}

挂载到 Controller:

typescript
@Controller('admin')
@UseGuards(AuthGuard)
export class AdminController {}

三、读 metadata 实现角色控制 ​

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

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

  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>('roles', [
      ctx.getHandler(),    // 方法级
      ctx.getClass(),      // 类级
    ]);
    if (!required) return true;

    const { user } = ctx.switchToHttp().getRequest();
    return required.some(r => user.roles?.includes(r));
  }
}

定义元数据装饰器:

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

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

使用:

typescript
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)         // 先验 token,再判角色
export class AdminController {
  @Get()
  @Roles('admin')
  list() { /* ... */ }

  @Get('users')
  @Roles('admin', 'super')
  users() { /* ... */ }
}

Guard + Reflector 工作机制详解:

执行链路:

请求 /admin/users
   ↓
NestJS 拦截请求 → 触发 Guard
   ↓
RolesGuard.canActivate()
   ├─ reflector.getAllAndOverride('roles', [方法, 类])
   │     ↓
   │   1. 先查方法级 @SetMetadata('roles', ...)
   │   2. 查不到再查类级 @SetMetadata
   │   3. 方法级覆盖类级
   │
   ├─ required = ['admin', 'super']
   ├─ 读 req.user.roles = ['user', 'admin']
   ├─ required.some(r => user.roles.includes(r))
   └─ return true (有交集,放行)

getAllAndOverride 行为:

方法级 @Roles类级 @Roles最终生效
['user']['admin']['user'] (方法覆盖类)
无['admin']['admin']
['user']无['user']
无无undefined(放行)

getAllAndOverride vs getAllAndMerge:

typescript
// override: 类级['admin'],方法级['user'] → 最终 ['user'](方法覆盖类)
const a = reflector.getAllAndOverride('roles', [handler, cls]);

// merge: 类级['admin'],方法级['user'] → 最终 ['admin', 'user'](合并)
const b = reflector.getAllAndMerge('roles', [handler, cls]);

@Roles() 不需要注册:

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

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// ^^^^^^^^^^^^^^^^
//                SetMetadata 自带"注册"能力,不用去任何地方声明

就两步,没有第三步:

步骤操作
1写 roles.decorator.ts(一行 SetMetadata)
2在 Controller / 方法上 @Roles(...)

SetMetadata 内部干了什么?

typescript
// 你写:
@Roles('admin', 'user')
class UsersController {}

// 等价于:
SetMetadata('roles', ['admin', 'user'])(UsersController)
// ↑ 在类的元数据里存了 { roles: ['admin', 'user'] }

// reflector.get('roles', UsersController) → ['admin', 'user']

记忆口诀:

  • @Roles() = 一个"贴标签"的小工具
  • SetMetadata = 标签内容写入类的元数据
  • Reflector = 读取这些标签(getAllAndOverride 方法优先)
  • 不需要"注册"装饰器,写出来直接 import 用

四、全局 Guard ​

typescript
// main.ts
app.useGlobalGuards(new JwtAuthGuard());

或者用 DI:

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

五、ExecutionContext 能干什么 ​

typescript
canActivate(ctx: ExecutionContext) {
  const http = ctx.switchToHttp();           // HTTP
  const ws = ctx.switchToWs();               // WebSocket
  const rpc = ctx.switchToRpc();             // Microservice

  const req = http.getRequest();
  const handler = ctx.getHandler();          // 当前方法
  const cls = ctx.getClass();                // 当前类
  return true;
}

同一个 Guard 可以给 HTTP、WS、RPC 三种场景复用。

六、Guard vs Middleware 区别 ​

MiddlewareGuard
时机最早在 Pipe 之前,Controller 之前
能拿到req/resExecutionContext(更结构化)
用途通用(req/res 处理)业务级权限(基于元数据)
路由元数据不可读可读

⚠️ 坑 1:在 Guard 里读 body 字段 → 不要,改用 Pipe 校验;Guard 只判断能不能进。

七、Guard 里抛异常 ​

typescript
canActivate(ctx: ExecutionContext): boolean {
  const req = ctx.switchToHttp().getRequest();
  if (!req.user) throw new UnauthorizedException('请先登录');
  if (!req.user.roles.includes('admin')) throw new ForbiddenException('无权限');
  return true;
}

抛异常后,全局过滤器统一处理响应格式。

八、实战:JWT Guard 骨架 ​

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

@Injectable()
export class JwtGuard implements CanActivate {
  constructor(private readonly jwt: JwtService) {}

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest();
    const token = (req.headers.authorization ?? '').replace('Bearer ', '');
    if (!token) throw new UnauthorizedException('no token');

    try {
      req.user = await this.jwt.verifyAsync(token);
      return true;
    } catch {
      throw new UnauthorizedException('invalid token');
    }
  }
}

挂载:

typescript
@Controller('api')
@UseGuards(JwtGuard)
export class ApiController {}

⚠️ 坑 2:canActivate 是 async 时,NestJS 会 await,但返回值必须是 boolean | Promise<boolean> | Observable<boolean>,不要返回 void。 ⚠️ 坑 3:@UseGuards(GuardClass) 是函数式传 class;传实例 @UseGuards(new Guard()) 会失去 DI。

九、公开路由(@Public 元数据) ​

typescript
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC, true);

// jwt.guard.ts
canActivate(ctx: ExecutionContext) {
  const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [
    ctx.getHandler(), ctx.getClass(),
  ]);
  if (isPublic) return true;
  // 正常校验
  return /* ... */;
}

// 标记
@Public()
@Get('login')
login() {}

十、本章小结 ​

要点关键
用途权限拦截(登录/角色)
实现CanActivate.canActivate 返回 boolean
元数据Reflector.getAllAndOverride
自定义装饰器SetMetadata('key', value)
全局APP_GUARD 提供者
公开路由@Public() 跳过校验

动手练习 ​

  1. AuthGuard:写 AuthGuard,无 token 抛 401
  2. RolesGuard:写 RolesGuard + @Roles('admin') 装饰器
  3. 混合:让 UsersController 用 JwtGuard + RolesGuard 双重守卫

下一章:第 10 章:拦截器 →

本站基于 VitePress 构建 · 由 StackHub 团队维护