938 字
5 分钟
NestJS 学习记录 Part 1:核心原理解析
为什么能在 request 中拿到 user 呢?
在实现 RolesGuard 时,常常需要在 request 对象中获取 user 对象:
@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { const request = context .switchToHttp() .getRequest<{ user?: { roles?: string[] } }>(); const user = request.user;
// 省略后续鉴权逻辑... }}这实际上是由 NestJS 和 Passport.js 构筑的身份验证流水线完成的。一切的源头在 JwtStrategy:
@Injectable()export class JwtStrategy extends PassportStrategy(Strategy) { constructor(configService: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get<string>("JWT_SECRET")!, }); }
/** * 解析 JWT payload,返回值会挂载到 req.user */ validate(payload: JwtPayload) { return { id: payload.sub, username: payload.username, roles: payload.roles || [], }; }}- 拦截请求:
JwtAuthGuard率先拦截请求,提取Authorization头的 Token。 - 验证与解析:底层触发
JwtStrategy的validate方法。 - 挂载上下文:Passport 验证成功后,自动将
validate返回的对象赋值给request.user。 - 权限校验:后续的
RolesGuard从request.user提取身份信息进行鉴权。
思考与对比
- 为什么这么做?
基于“关注点分离”原则。身份验证(解析 Token)和授权控制(校验权限)分离。将解析 Token 抽离到 Strategy 中,
Guard保持轻量和复用。 - 如果在 Controller 里直接解析 Token 会怎样? 会导致代码冗余。每个需要鉴权的接口都要写解码 Token 的逻辑,业务与鉴权强耦合,后续重构成本高。
装饰器到底是按照什么顺序执行的?
控制器上经常会有多个装饰器,比如我的项目中是这样写的:
@ApiTags("用户管理 (Admin)") // 5. 最后执行@ApiBearerAuth() // 4. 第四个执行@UseGuards(JwtAuthGuard, RolesGuard) // 3. 第三个执行@Roles(RoleEnum.ADMIN) // 2. 第二个执行@Controller("user") // 1. 最先执行export class UserController {}思考与对比
- 代码加载阶段遵循 TypeScript 规范,采用洋葱模型从下往上执行。此时,装饰器并不执行业务逻辑,仅调用
Reflect.defineMetadata添加元数据。 - 运行时阶段由 NestJS 框架接管请求的生命周期。比如
@UseGuards(JwtAuthGuard, RolesGuard)内部是从左到右依次执行,因为存在依赖关系(先登录,后鉴权)。像@ApiTags仅在应用启动扫描 Swagger 时生效。 - 这种设计的好处是声明式编程。开发者通过装饰器声明接口属性和权限,而非编写大量的
if-else判断逻辑,提高了代码的直观性。
switchToHttp() 到底是干嘛用的?
在编写 Guard 或 Interceptor 时,NestJS 提供的是 context: ExecutionContext。
const request = context.switchToHttp().getRequest();思考与对比
- 为什么这么设计? NestJS 是一个与传输层无关的框架。同一套业务代码可以支撑 HTTP、WebSockets、微服务 (TCP/gRPC) 等。
- 为什么要
switchToHttp()? NestJS 通过ExecutionContext将底层协议统一。调用switchToHttp()明确获取 HTTP 上下文。如果增加 WebSocket 模块,只需改为context.switchToWs(),即可复用守卫逻辑。 - 避免直接注入
@Req(): 大量依赖express的原生请求对象,会导致代码和 HTTP 协议深度绑定,降低后续向微服务架构演进的灵活性。
getAllAndOverride 和 getAllAndMerge 有啥区别?
在 RolesGuard 中,我们需要获取当前路由需要的角色信息。当 Controller 类和具体的 Route 方法都加了 @Roles 装饰器时,该听谁的?
思考与对比
-
getAllAndOverride(覆盖优先): 我们在项目中使用的是这个方法:apps/server/src/auth/guards/roles.guard.ts const requiredRoles = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY,[context.getHandler(), context.getClass()],);适用场景:局部特例。例如整个
UserController默认需要ADMIN,但某个特定接口允许USER访问。方法级配置覆盖类级配置(因为getHandler()传在前面)。 -
getAllAndMerge(合并累加):const roles = this.reflector.getAllAndMerge<RoleEnum[]>("roles", [context.getHandler(),context.getClass(),]);适用场景:多重限制叠加。整个模块要求
ADMIN,而某个接口贴了SUPER_ADMIN,合并后要求同时具备['ADMIN', 'SUPER_ADMIN'],全部满足才能放行。
NestJS 学习记录 Part 1:核心原理解析
https://nollieleo.github.io/posts/nestjs学习记录-part1/