守卫 Guard
本教程共 47 篇 · 第 17 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:搞懂守卫是什么、什么时候执行、怎么写,学完你能给任意路由加上”门禁卡”机制。
守卫是什么
想象一下小区的门禁系统。保安不关心你从哪来、穿什么衣服,他只检查一件事——你有没有门禁卡。有卡放行,没卡拦住。
守卫干的就是这个活。
在 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;
}
}
这段代码做了三件事:
- 从请求头提取 Bearer Token
- 用 JwtService 验证 Token 合法性
- 验证通过后,把解析出的用户信息挂到
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+ 自定义装饰器实现灵活的角色/权限控制 - 多个守卫按顺序执行,各自保持单一职责
下一章我们聊聊拦截器,看看怎么在请求执行过程中插入自定义逻辑。