17-1 鉴权守卫:设置鉴权用户可访问的控制器(作业)
一、守卫核心概念
1.1 守卫的本质与作用
守卫是使用 @Injectable()
装饰的类,通过 NestJS 的依赖注入(DI)容器管理实例化。其主要职责是根据运行时条件(如权限、角色、访问控制列表 ACL 等)决定是否允许访问特定的路由处理程序。
核心特点:
- 声明式控制:通过装饰器
@UseGuards()
以声明方式绑定到控制器或路由方法。 - 生命周期位置:在请求处理管道中,守卫的执行顺序位于 中间件之后,但在 拦截器(Interceptors)和管道(Pipes)之前。
- 职责单一:专注于权限校验,不涉及业务逻辑处理。
💡 提示:守卫通常与策略模式(如 Passport.js 的 AuthGuard
)结合使用,实现灵活的认证与授权逻辑。
常见应用场景:
- 身份验证(Authentication):检查用户是否登录(如 JWT 校验)。
- 授权(Authorization):验证用户角色或权限(如 Admin 权限校验)。
- 访问控制(ACL):基于动态规则限制资源访问(如 VIP 用户专属接口)。
1.2 守卫与中间件的区别
虽然守卫和中间件(Middleware)都可用于请求拦截,但二者在功能和使用场景上有显著差异:
对比表格:
特性 | 中间件(Middleware) | 守卫(Guards) |
---|---|---|
执行时机 | 最早执行,在路由匹配前 | 在中间件之后,拦截器和管道之前 |
上下文访问 | 只能访问 Request 和 Response 对象 | 可获取完整的 ExecutionContext (执行上下文) |
职责范围 | 通用请求处理(如日志、CORS、数据转换) | 专注于权限控制 |
模块化 | 全局或模块级注册 | 通常绑定到特定控制器或方法 |
流程图解:
关键差异点:
- 上下文感知能力
- 中间件无法知道
next()
后具体执行哪个路由处理程序。 - 守卫通过
ExecutionContext
能精确获取目标控制器和方法信息。
- 中间件无法知道
- 适用场景
- 中间件适合处理与业务无关的通用逻辑(如请求日志)。
- 守卫适合实现与业务强相关的权限校验(如角色检查)。
💡 提示:在需要动态权限控制的场景(如 RBAC),优先选择守卫;若需全局预处理请求(如压缩响应),则使用中间件。
扩展学习资源
- NestJS 官方文档:Guards 章节
- 实战案例:
- 调试工具:
- 使用
console.log(context.getClass())
查看守卫的目标控制器类。 - 通过
context.getHandler()
获取当前路由方法元数据。
- 使用
通过深入理解守卫的核心概念,可以更灵活地设计安全的 API 访问控制层! 🚀
二、自定义守卫实现
2.1 创建AdminGuard
命令详解
nest g guard common/guards/admin --flat --no-spec
bash
--flat
:不创建子目录--no-spec
:不生成测试文件- 推荐路径:
common/guards/
集中管理所有守卫
生成文件解析
import { CanActivate, ExecutionContext } from '@nestjs/common';
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
return true; // 默认放行
}
}
typescript
💡 进阶技巧:
- 异步守卫:使用
Promise<boolean>
async canActivate(context: ExecutionContext): Promise<boolean>
typescript
- 全局守卫:通过
APP_GUARD
注册
providers: [
{ provide: APP_GUARD, useClass: AdminGuard }
]
typescript
2.2 核心功能实现
2.2.1 获取请求对象
const request = context.switchToHttp().getRequest();
typescript
支持多种协议:
调试技巧:
console.log({
headers: request.headers,
method: request.method,
url: request.url
});
typescript
2.2.2 权限校验逻辑
基础实现
const user = request.user;
return user.role === 'admin';
typescript
增强版(带错误处理)
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.user) {
throw new UnauthorizedException('请先登录');
}
if (request.user.role !== 'admin') {
throw new ForbiddenException('需要管理员权限');
}
return true;
}
typescript
动态权限校验
import { Reflector } from '@nestjs/core';
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
return roles.includes(request.user.role);
}
typescript
2.3 控制器应用守卫
基础用法
@UseGuards(AuthGuard('jwt'), AdminGuard)
typescript
进阶模式
- 条件守卫:
@UseGuards(
process.env.NODE_ENV === 'production'
? [AuthGuard, AdminGuard]
: [DevGuard]
)
typescript
- 自定义装饰器:
// roles.decorator.ts
export const Roles = (...roles: string[]) =>
SetMetadata('roles', roles);
// 使用
@Roles('admin', 'superadmin')
@UseGuards(RolesGuard)
typescript
- 混合应用:
@Controller('users')
export class UsersController {
// 控制器级别守卫
@UseGuards(AuthGuard)
constructor() {}
// 方法级别覆盖
@Get('admin')
@UseGuards(AdminGuard)
adminOnly() {}
}
typescript
守卫执行顺序可视化
最佳实践建议
- 守卫拆分原则:
- 认证守卫:只验证身份(如JWT校验)
- 授权守卫:处理业务权限(如角色校验)
- 性能优化:
// 缓存权限数据
private roleCache = new Map<string, boolean>();
async canActivate(context: ExecutionContext) {
const userId = request.user.id;
if (this.roleCache.has(userId)) {
return this.roleCache.get(userId);
}
// ...校验逻辑
}
typescript
- 测试方案:
// 单元测试示例
it('should reject non-admin users', () => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({ user: { role: 'user' } })
})
};
expect(guard.canActivate(mockContext)).toBeFalsy();
});
typescript
通过以上扩展,开发者可以构建出灵活、健壮的权限控制系统,适应从简单到复杂的各种业务场景。
三、守卫高级应用
3.1 多守卫执行机制
执行流程深度解析
关键特性详解:
- 顺序控制:
- 装饰器声明顺序决定执行顺序
@UseGuards(Guard1, Guard2, Guard3) // 执行顺序1→2→3
typescript - 短路机制:
- 任一守卫返回
false
或抛出异常即终止后续守卫执行 - 典型应用:先验证登录态,再校验权限
@UseGuards(AuthGuard, PermissionGuard)
typescript - 任一守卫返回
- 数据共享方案:
- 通过
request
对象传递数据
// Guard1 request.user = { id: 123, role: 'admin' }; // Guard2 const user = request.user;
typescript - 通过
- 性能优化技巧:
- 将高频校验结果缓存到request对象
if (!request._cachedRoles) { request._cachedRoles = await fetchRoles(); }
typescript
实战场景示例:
电商平台订单接口保护
@UseGuards(
JwtAuthGuard, // 1.验证JWT
AccountStatusGuard, // 2.检查账号状态
PaymentGuard, // 3.验证支付状态
InventoryGuard // 4.检查库存
)
@Post('orders')
createOrder() {}
typescript
3.2 守卫依赖注入
完整依赖注入方案
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
private userService: UserService,
private reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
const user = await this.userService.getCurrentUser(context);
return roles.includes(user.role);
}
}
typescript
模块配置要点:
- 服务提供方模块:
@Module({ providers: [UserService], exports: [UserService] // 必须导出 }) export class UserModule {}
typescript - 守卫使用方模块:
@Module({ imports: [UserModule], // 导入服务模块 providers: [AdminGuard] }) export class OrderModule {}
typescript
常见问题解决方案:
- 循环依赖:
- 使用
forwardRef()
解决模块间循环引用
@Module({ imports: [forwardRef(() => UserModule)] })
typescript - 使用
- 全局服务注入:
// 在根模块注册 @Global() @Module({ providers: [CommonService], exports: [CommonService] })
typescript - 动态注入:
constructor( @Inject(REQUEST) private request: Request, @Inject('CONFIG') private config: any ) {}
typescript
调试技巧:
- 检查注入关系:
console.log(this.userService.constructor.name);
typescript - 模拟注入测试:
const testGuard = new AdminGuard(mockUserService);
typescript
扩展应用:守卫与拦截器协作
通过深入理解守卫的高级应用,可以构建出灵活可靠的系统权限体系! 🛡️
四、实践注意事项
4.1 正确获取用户信息
4.1.1 JWT守卫执行顺序
深度解析:
@UseGuards(AuthGuard('jwt'), AdminGuard) // 正确顺序
typescript
- 执行流程:
AuthGuard
先验证JWT并解析request.user
AdminGuard
读取解析后的用户信息进行权限校验
错误示范分析:
@UseGuards(AdminGuard, AuthGuard('jwt')) // 反模式
typescript
会导致AdminGuard
读取到未解析的undefined
用户对象
进阶方案:
// 自定义组合守卫
export class JwtAdminGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext) {
await super.canActivate(context); // 先执行JWT验证
return checkAdminRole(context); // 再执行权限校验
}
}
typescript
4.1.2 避免空用户引用
防御性编程方案:
// 最佳实践:使用类型守卫
function hasValidUser(req: Request): req is AuthenticatedRequest {
return !!req.user && typeof req.user.id === 'string';
}
if (!hasValidUser(request)) {
throw new UnauthorizedException('Invalid user session');
}
typescript
用户信息增强模式:
// 在JWT守卫中扩展用户对象
request.user = {
...payload,
permissions: await getPermissions(payload.sub)
};
typescript
4.2 模块化最佳实践
扩展最佳实践表格:
实践原则 | 错误示例 | 正确解决方案 | 原理说明 |
---|---|---|---|
服务共享 | 在A/B模块都声明UserService | 创建SharedModule 导出服务 | 避免内存中存在多个服务实例 |
守卫依赖 | 在OrderModule 直接使用UserGuard | 在OrderModule 导入UserModule | 确保依赖注入链完整 |
全局守卫注册 | 在多模块重复providers: [AuthGuard] | 在根模块使用APP_GUARD | 单例模式提升性能 |
动态守卫配置 | 硬编码权限规则 | 使用Reflector 读取元数据 | 提高代码可维护性 |
跨模块通信 | 直接导入服务实例 | 通过事件总线(EventEmitter )通信 | 解耦模块依赖 |
模块关系示意图:
4.3 调试技巧
增强调试方案:
- 上下文深度检查:
console.dir({
handler: context.getHandler().name,
class: context.getClass().name,
request: {
headers: request.headers,
user: request.user
}
}, { depth: null });
typescript
- 结构化错误信息:
throw new ForbiddenException({
error: 'PERMISSION_DENIED',
requiredRole: 'admin',
userRole: request.user?.role,
timestamp: new Date().toISOString()
});
typescript
- 调试工具链:
- 使用NestJS的
Logger
记录完整执行上下文
private logger = new Logger('GuardDebug'); this.logger.debug(`Attempt by user ${request.user.id}`);
typescript- 配合Postman的Tests脚本自动验证守卫逻辑
pm.test("Admin required", () => { pm.response.to.have.status(403); pm.expect(pm.response.json().error).to.eql("PERMISSION_DENIED"); });
javascript - 使用NestJS的
- 性能监控:
const start = Date.now();
const result = await next();
console.log(`Guard耗时: ${Date.now() - start}ms`);
return result;
typescript
4.4 安全增强建议
- 敏感操作二次验证:
@UseGuards(JwtGuard, AdminGuard, OTPGuard) // 关键操作需要多重验证
@Post('delete-account')
deleteAccount() {}
typescript
- 防爆破设计:
// 在守卫中添加速率限制
const attempts = await cache.get(`login_attempts_${ip}`);
if (attempts > 5) {
throw new TooManyRequestsException();
}
typescript
- 审计日志集成:
@UseGuards(AuthGuard, AuditGuard) // 审计守卫记录操作日志
@Patch('user/:id')
updateUser() {}
typescript
通过以上扩展实践,可以构建出既安全又易于维护的守卫系统! 🔒
↑