第 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 区别
| Middleware | Guard | |
|---|---|---|
| 时机 | 最早 | 在 Pipe 之前,Controller 之前 |
| 能拿到 | req/res | ExecutionContext(更结构化) |
| 用途 | 通用(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() 跳过校验 |
动手练习
- AuthGuard:写
AuthGuard,无 token 抛 401 - RolesGuard:写
RolesGuard+@Roles('admin')装饰器 - 混合:让
UsersController用JwtGuard + RolesGuard双重守卫
下一章:第 10 章:拦截器 →