一个被标记为"受保护"的管理端接口,在代码评审里被翻了出来。控制器类上挂着 @UseGuards(RolesGuard),RolesGuard 注入了 UserService 用来检查角色层级,main.ts 里还用 app.useGlobalGuards(new RolesGuard(reflector, userService)) 把它注册成了全局守卫。看起来滴水不漏。
它同时也是坏的:userService 在守卫内部是 undefined,每一个请求都是如此。更麻烦的是没有任何测试能抓到它——应用从未崩溃,它只是让每个请求都走进了那条"假定用户没有高级角色"的分支。
![]()
这里没有一处拼写错误。守卫类是对的,装饰器是对的。唯一错的是守卫的注册方式。而在你搞清楚守卫对 Nest 容器来说到底是什么之前,那行代码看上去完全合理。
守卫是一个普通的依赖注入提供者
把心智模型压缩成一句话:NestJS 守卫就是一个普通的 DI 提供者——和服务是同一类东西——只是额外实现了一个方法 canActivate(context: ExecutionContext)。Nest 会在即将调用路由处理器之前调用它。
如果 canActivate 解析为 true,请求继续流向拦截器、管道和处理器。如果解析为 false 或抛出异常,处理器永远不会执行,Nest 直接把请求交给异常过滤器层。
这一句话推出三个结论,恰好就是开头那个 bug 踩中的三件事:
- 守卫是提供者,所以它必须由 Nest 的容器来构造,依赖才会被真正注入。
- 守卫在路由处理器之前运行,所以它拿不到处理器执行后的任何结果。
- 守卫的返回值决定请求是否继续,所以它不能"什么都不做"——沉默等于放行或拦截。
开头那行 app.useGlobalGuards() 接收的是一个实例。这个实例完全不经过 Nest 的依赖注入:你在任何模块之外、用 new 亲手构造了它,那时应用的提供者甚至还不存在,没法交出一个真正的 UserService。Nest 会照常对每个请求调用这个对象的 canActivate(),只是它给不了这个守卫本来要用的依赖。
一个能编译、也真能干活的守卫
最简单的守卫长这样:用 @Injectable() 装饰一个类,实现 CanActivate,在 canActivate 里通过 context.switchToHttp().getRequest() 拿到请求对象,返回一个布尔值。把它挂到单个路由上,用 @UseGuards(AuthGuard) 即可。
关键点在于,canActivate 可以返回 boolean、Promise 或 Observable,Nest 会等待或订阅你给出的任何一种。一个守卫去查数据库或外部身份提供方来校验会话,是完全正常的做法——只要写成 async canActivate(...): Promise,Nest 会在做决定前等它。
ExecutionContext 回答两个不同的问题
ExecutionContext 是每个守卫、拦截器和异常过滤器都会收到的唯一参数,它同时回答两件事。
第一件:这是什么类型的调用,底层请求对象是什么。通过 switchToHttp()、switchToRpc() 或 switchToWs() 区分。大多数守卫只会调用 context.switchToHttp().getRequest(),但同一个守卫类也可以挡在 WebSocket 网关或微服务处理器前面——只要先用 context.getType() 判断再分支。这正是它被称为"上下文"而不只是 HTTP 请求包装器的原因。
第二件:Nest 即将调用哪个处理器、哪个类。通过 context.getHandler()(具体的路由方法,一个函数引用)和 context.getClass()(控制器类)获取。这一半能力让元数据驱动的守卫成为可能,因为这是守卫唯一能问出"当前这个具体路由身上有没有挂 @Roles(...) 装饰器"的途径。守卫按注册只运行一次,但 getHandler() 和 getClass() 会告诉它,此刻正在为哪个路由做判断。
把这两半拼起来,就能理解 RolesGuard 为什么必须拿到 Reflector 和 UserService:Reflector 负责从 getHandler() 指向的那个方法上读出 roles 元数据,UserService 负责拿请求里的 user 去比对角色层级。少了后者,检查角色这一步就无从谈起。
回到开头那个接口。修复方案不是换一个守卫,而是换一种注册方式。原因现在已经清楚了:守卫是提供者,提供者必须由容器构造,容器构造时才会注入依赖。用 new 手工造出来的实例,绕开了整条注入链路,也就绕开了这个守卫存在的全部意义。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.