NestJS 守卫 (Guard) 完整指南
一、什么是守卫?
守卫(Guard) 是 NestJS 中用于实现 认证(Authentication) 和 授权(Authorization) 的核心机制。它像一道安全门禁,在请求到达控制器之前进行拦截和验证。
守卫的核心职责
- ✅ 身份验证:确认请求者的身份(你是谁?)
- ✅ 权限检查:验证是否有权访问该资源(你能做什么?)
- ✅ 访问控制:基于角色、权限、IP 等进行精细化控制
守卫 vs 中间件
| 特性 | 中间件 | 守卫 |
|---|---|---|
| 执行时机 | 请求生命周期最早期 | 中间件之后,管道/拦截器之前 |
| 环境感知 | ❌ 不知道将执行哪个控制器 | ✅ 通过 ExecutionContext 精确感知 |
| 元数据访问 | ❌ 无法读取装饰器元数据 | ✅ 通过 Reflector 读取元数据 |
| 最佳用途 | 通用预处理(日志、CORS 等) | 认证和授权逻辑 |
二、守卫的工作原理
执行流程图
HTTP 请求
│
▼
┌─────────────┐
│ 中间件 │ ← 日志、CORS、请求解析等
└──────┬──────┘
│
▼
┌─────────────┐
│ 全局守卫 │ ← 最先执行
└──────┬──────┘
│
▼
┌─────────────┐
│ 控制器守卫 │ ← 控制器级别
└──────┬──────┘
│
▼
┌─────────────┐
│ 方法守卫 │ ← 路由方法级别(最后执行)
└──────┬──────┘
│
▼
┌─────────────┐
│ 管道/拦截器 │ ← 数据验证、响应转换
└──────┬──────┘
│
▼
┌─────────────┐
│ 控制器 │ ← 业务逻辑执行
└──────┬──────┘
│
▼
响应
⚠️ 注意:多个守卫按照 全局 → 控制器 → 方法 的顺序执行。
守卫的核心接口
每个守卫都必须实现 CanActivate 接口:
typescriptinterface CanActivate { canActivate(context: ExecutionContext): boolean | Promise | Observable; }
- 返回
true:请求放行 - 返回
false:请求被拒绝,返回 403 状态码
三、快速上手:第一个守卫
创建守卫
typescriptimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class SimpleGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise | Observable { const request = context.switchToHttp().getRequest(); const token = request.headers['authorization']; // 简单验证:检查是否携带 token return !!token; } }
使用守卫
typescriptimport { Controller, Get, UseGuards } from '@nestjs/common'; @Controller('protected') export class ProtectedController { @Get('data') @UseGuards(SimpleGuard) getProtectedData() { return { message: '只有携带 token 才能看到我!' }; } }
四、守卫的三种绑定方式
1️⃣ 方法级守卫
仅对单个路由方法生效:
typescript@Post('create') @UseGuards(AuthGuard) createPost(@Body() dto: CreatePostDto) { return this.postService.create(dto); }
2️⃣ 控制器级守卫
对整个控制器所有路由生效:
typescript@UseGuards(AuthGuard) @Controller('admin') export class AdminController { @Get('dashboard') // ✅ 受保护 getDashboard() {} @Get('settings') // ✅ 受保护 getSettings() {} }
3️⃣ 全局守卫
对整个应用所有路由生效,有两种注册方式:
方式一:在 main.ts 中注册
typescriptconst app = await NestFactory.create(AppModule); app.useGlobalGuards(new AuthGuard()); await app.listen(3000);
方式二:在模块中注册(推荐)
typescriptimport { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; @Module({ providers: [ { provide: APP_GUARD, useClass: AuthGuard, }, ], }) export class AppModule {}
✅ 推荐使用方式二:支持依赖注入,便于单元测试。
五、实战:JWT 身份认证守卫
1. 安装依赖
bashnpm install @nestjs/jwt @nestjs/passport passport passport-jwt npm install -D @types/passport-jwt
2. 创建 JWT 策略
typescriptimport { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET || 'your-secret-key', ignoreExpiration: false, }); } async validate(payload: any) { // payload 来自 JWT 的解码内容 return { userId: payload.sub, username: payload.username, roles: payload.roles }; } }
3. 创建 JWT 认证守卫
typescriptimport { 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 { const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException('请先登录'); } try { const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET, }); // 💡 将用户信息挂载到 request 上,供后续使用 request.user = payload; return true; } catch { throw new UnauthorizedException('Token 已过期或无效'); } } private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } }
4. 使用守卫
typescript@Controller('user') export class UserController { @Get('profile') @UseGuards(JwtAuthGuard) getProfile(@Req() req: Request) { // req.user 已包含用户信息 return { message: '获取用户信息成功', user: req.user }; } }
六、实战:基于角色的权限守卫(RBAC)
在大厂项目中,RBAC(基于角色的访问控制) 是最常见的权限管理模式。
1. 定义角色枚举
typescriptexport enum Role { User = 'user', Admin = 'admin', SuperAdmin = 'super_admin', }
2. 创建角色装饰器
typescriptimport { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
3. 实现角色守卫
typescriptimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from './roles.decorator'; import { Role } from './role.enum'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // 获取路由所需的角色 const requiredRoles = this.reflector.getAllAndOverride( ROLES_KEY, [ context.getHandler(), // 方法级别的元数据 context.getClass(), // 控制器级别的元数据 ], ); // 没有角色要求,直接放行 if (!requiredRoles || requiredRoles.length === 0) { return true; } // 获取当前用户(由认证守卫或中间件注入) const { user } = context.switchToHttp().getRequest(); if (!user) { return false; } // 检查用户是否拥有所需角色之一 return requiredRoles.some((role) => user.roles?.includes(role)); } }
4. 组合使用
typescript@Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) // 先认证,再授权 @Roles(Role.Admin) // 控制器级别角色要求 export class AdminController { @Get('dashboard') getDashboard() { return { message: '管理员仪表盘' }; } @Post('delete-user') @Roles(Role.SuperAdmin) // 方法级别覆盖,仅超级管理员可操作 deleteUser(@Body('userId') userId: string) { return { message: `用户 ${userId} 已删除` }; } @Get('public') @Roles() // 空数组:无需角色,任何人都可访问 getPublicInfo() { return { message: '公开信息' }; } }
七、大厂最佳实践
🏆 原则一:职责单一
守卫只做一件事:判断请求是否放行。数据库查询、业务逻辑等应放在 Service 层。
typescript// ❌ 错误示例:在守卫中执行复杂业务逻辑 @Injectable() export class BadGuard implements CanActivate { async canActivate(context: ExecutionContext) { const user = request.user; const posts = await this.postService.findByUser(user.id); const hasPermission = posts.some(p => p.canDelete); // 业务逻辑应该在 Service 中 return hasPermission; } } // ✅ 正确示例:只做判断 @Injectable() export class GoodGuard implements CanActivate { canActivate(context: ExecutionContext) { const { user } = context.switchToHttp().getRequest(); return user?.permissions?.includes('post:delete'); } }
🏆 原则二:抛出异常而非返回 false
返回 false 只会返回 403 状态码,无法提供具体错误信息。结合 ExceptionFilter 统一处理:
typescriptimport { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const token = request.headers.authorization; // ❌ 不推荐 // if (!token) return false; // ✅ 推荐:抛出具体异常 if (!token) { throw new UnauthorizedException('缺少认证令牌'); } // ... 验证逻辑 return true; } }
🏆 原则三:优先使用依赖注入
避免用 new 手动实例化守卫,始终通过构造函数注入依赖:
typescript// ❌ 不推荐 const guard = new AuthGuard(new JwtService(), new Reflector()); // ✅ 推荐:由 NestJS 容器管理 @Controller() @UseGuards(AuthGuard) // Nest 自动注入依赖 export class MyController {}
🏆 原则四:使用 Passport 做认证
NestJS 官方推荐使用 @nestjs/passport 和 passport-jwt 处理认证,而不是手动解析 Token:
typescript// ✅ 使用 Passport 策略 @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { // 通过扩展 Passport 守卫,获得更好的错误处理和可维护性 }
🏆 原则五:守卫组合模式
一个请求可能需要多个守卫组合工作:
typescript// 组合守卫:先认证,再授权 @UseGuards(JwtAuthGuard, RolesGuard, IpWhitelistGuard) @Controller('admin') export class AdminController {}
守卫按数组顺序执行,任何一个返回 false 都会阻止请求。
🏆 原则六:性能优化
守卫在请求路径上,应保持轻量:
typescript// ✅ 缓存角色信息,避免重复查询 @Injectable() export class RolesGuard implements CanActivate { private roleCache = new Map(); // 考虑使用 Redis async canActivate(context: ExecutionContext): Promise { const user = context.switchToHttp().getRequest().user; let roles = this.roleCache.get(user.id); if (!roles) { roles = await this.roleService.findByUserId(user.id); this.roleCache.set(user.id, roles); } return requiredRoles.some(r => roles.includes(r)); } }
八、常见问题与解决方案
❓ 问题 1:@UseGuards() 没有生效?
排查清单:
- 守卫类是否添加了
@Injectable()装饰器? - 守卫是否在模块中正确注册(如果使用了依赖注入)?
- 请求头是否携带了正确的
Authorization: Bearer? - 是否在
main.ts中误用了app.useGlobalGuards()覆盖了模块配置?
❓ 问题 2:request.user 为空?
解决方案:
typescript// 1. 检查 Passport 策略是否正确返回 async validate(payload: any) { // 这个返回值会成为 request.user return { userId: payload.sub, username: payload.username }; } // 2. 确保守卫执行顺序正确 @UseGuards(JwtAuthGuard, RolesGuard) // ✅ JwtAuthGuard 先执行
❓ 问题 3:守卫执行多次?
原因:守卫在全局、控制器、方法多个级别重复绑定。
解决方案:遵循单一绑定原则,根据需求选择合适的绑定层级。
❓ 问题 4:测试守卫时出现依赖注入错误?
typescript// ✅ 正确的单元测试方式 describe('AuthGuard', () => { let guard: AuthGuard; let jwtService: jest.Mocked; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ AuthGuard, { provide: JwtService, useValue: { verifyAsync: jest.fn(), }, }, ], }).compile(); guard = module.get(AuthGuard); jwtService = module.get(JwtService); }); it('should throw if no token', async () => { const context = createMockExecutionContext({ headers: {} }); await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); }); });
九、GraphQL 中的守卫
守卫同样适用于 GraphQL 解析器:
typescriptimport { Query, Resolver, Context } from '@nestjs/graphql'; @Resolver() export class UserResolver { @Query(() => User) @UseGuards(JwtAuthGuard) getProfile(@Context() context: any) { const { req } = context; return req.user; } @Mutation(() => Boolean) @UseGuards(RolesGuard) @Roles(Role.Admin) deleteUser(@Args('id') id: string) { return this.userService.delete(id); } }
在 GraphQL 上下文中获取请求对象的方式略有不同:
typescript// 守卫中适配 GraphQL const context = context.switchToHttp().getRequest(); // 或使用 GqlExecutionContext const ctx = GqlExecutionContext.create(context); const { req } = ctx.getContext();
十、总结
📌 核心要点回顾
| 要点 | 说明 |
|---|---|
| 守卫职责 | 认证和授权,在请求进入控制器前拦截 |
| 执行时机 | 中间件之后,管道/拦截器之前 |
| 绑定方式 | 方法级、控制器级、全局级 |
| 核心方法 | `canActivate(context): boolean |
| 元数据读取 | 通过 Reflector 读取装饰器元数据 |
| 组合使用 | 多个守卫按顺序执行,全部通过才放行 |
🎯 最佳实践速记
- 职责单一:守卫只做判断,不写业务逻辑
- 抛出异常:用
throw new UnauthorizedException()替代return false - 依赖注入:避免手动
new,让 Nest 容器管理 - 使用 Passport:认证守卫优先基于
@nestjs/passport - 保持轻量:守卫不应有 I/O 阻塞操作
- 合理组合:先认证再授权的组合模式
加载中...