NestJS微服务:拦截出站HTTP请求并设置JWT令牌的安全疑问
问题解答
原方案的风险分析
你提到的全局拦截器修改Axios默认头的方案确实存在令牌混用的风险,你的担忧完全合理。因为httpService.axiosRef.defaults.headers.common是全局共享配置,当多个请求并发处理时,后一个请求的拦截器会覆盖全局Authorization头,导致前一个请求还未发出就使用了错误令牌,引发权限问题或数据泄露。
正确实现方案:基于AsyncLocalStorage的上下文隔离
要实现无参传递令牌且避免并发问题,可利用Node.js的AsyncLocalStorage为每个请求创建独立上下文,结合Axios请求拦截器动态注入令牌。
步骤1:创建请求上下文存储
// src/common/request-context.ts import { AsyncLocalStorage } from 'async_hooks'; import { Injectable } from '@nestjs/common'; @Injectable() export class RequestContext { private readonly als = new AsyncLocalStorage<Map<string, any>>(); run<T>(callback: () => T): T { return this.als.run(new Map(), callback); } set(key: string, value: any): void { const store = this.als.getStore(); if (store) store.set(key, value); } get(key: string): any { return this.als.getStore()?.get(key); } }
步骤2:全局拦截器存储令牌到上下文
// src/common/interceptors/auth-token.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { RequestContext } from '../request-context'; @Injectable() export class AuthTokenInterceptor implements NestInterceptor { constructor(private readonly requestContext: RequestContext) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { let token: string | undefined; // 处理HTTP请求 if (context.getType() === 'http') { const req = context.switchToHttp().getRequest(); token = req.headers['authorization']; } // 处理GraphQL请求 else if (context.getType() === 'graphql') { const gqlCtx = context.getArgByIndex(2); token = gqlCtx.token; } if (token) { this.requestContext.set('authorization', token); } return next.handle(); } }
步骤3:自定义HttpService注入令牌
// src/common/providers/custom-http.service.ts import { HttpService } from '@nestjs/axios'; import { Injectable, OnModuleInit } from '@nestjs/common'; import { RequestContext } from '../request-context'; @Injectable() export class CustomHttpService extends HttpService implements OnModuleInit { constructor(private readonly requestContext: RequestContext) { super(); } onModuleInit() { // 添加Axios请求拦截器,动态注入当前请求的令牌 this.axiosRef.interceptors.request.use((config) => { const token = this.requestContext.get('authorization'); if (token) { config.headers['Authorization'] = token; } return config; }); } }
步骤4:模块注册配置
// src/app.module.ts import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { HttpModule } from '@nestjs/axios'; import { RequestContext } from './common/request-context'; import { AuthTokenInterceptor } from './common/interceptors/auth-token.interceptor'; import { CustomHttpService } from './common/providers/custom-http.service'; import { UserController } from './user/user.controller'; import { VehicleService } from './vehicle/vehicle.service'; @Module({ imports: [HttpModule], controllers: [UserController], providers: [ RequestContext, { provide: APP_INTERCEPTOR, useClass: AuthTokenInterceptor, }, { provide: HttpService, useClass: CustomHttpService, }, VehicleService, ], }) export class AppModule {}
步骤5:简化业务代码
现在控制器和服务无需再传递令牌参数:
user.controller.ts
@Controller('/users') export class UserController { constructor(private userService: UserService, private vehicleService: VehicleService) {} @Post() create(@Body() userData: UserData) { const vehicle$ = this.vehicleService.getVehicleById(userData.vehicleId); // 后续保存用户逻辑 } }
vehicle.service.ts
@Injectable() export class VehicleService { basePath = `http://ms-vehicles/api/v1/vehicles`; constructor(private httpService: HttpService) {} getVehicleById(vehicleId: string) { return this.httpService.get(`${this.basePath}/${vehicleId}`); } }
方案优势
- 上下文隔离:每个请求的令牌存储在独立的AsyncLocalStorage上下文中,并发请求间不会互相干扰
- 无侵入性:业务代码无需关心令牌传递逻辑,降低耦合度
- 扩展性强:后续如需传递其他请求上下文数据,只需在RequestContext中添加即可
内容的提问来源于stack exchange,提问作者Ajantha Bandara
相关产品推荐
相关产品推荐

