NestJS出站请求拦截器添加及Authorization头缺失问题排查
我正在开发一个作为API与其他服务中间件的NestJS应用,需求是给每个出站请求添加Bearer Token,若Token被拒绝则获取新Token并重试请求。我用拦截器添加Authorization头,但目标服务一直返回403 Forbidden,排查后发现请求实际没携带这个头。
简化版拦截器代码:
intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest<Request>(); request.headers.Authorization = this.token; return next.handle(); }
这并非添加Authorization头的正确方式吗?
完整拦截器代码:
@Injectable() export class AuthTokenInterceptor implements NestInterceptor { private readonly authUrl = "https://..."; private token = ""; constructor(private readonly http: HttpService) { // 尽快获取Token this.trySaveToken(); } intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest<Request>(); if (!request.headers.Authorization) { request.headers.Authorization = this.token; } return next.handle().pipe( catchError((error: AxiosError) => { const originalRequest = error.config; console.log(originalRequest?.headers); // 这里没有Authorization头! // 如果是认证错误,大概率需要重新获取Token if (error.response?.status === 401 && originalRequest != null && originalRequest.url !== this.authUrl) { // 不加第二个判断会无限循环 this.trySaveToken(); originalRequest.headers.Authorization = this.token; // 为当前请求覆盖旧Token return of(originalRequest); // 用新Token重试请求 } else { return throwError(() => error); } }), } private trySaveToken() { this.http.post(this.authUrl).subscribe({ next: (response) => { this.token = 'Bearer ' + response.data.access_token; }, }); } }
我的重试逻辑可能还存在问题,但在解决Authorization头缺失问题前无法推进。
console.log输出内容:
Object [AxiosHeaders] { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'User-Agent': 'axios/1.6.8', 'Content-Length': '162', 'Accept-Encoding': 'gzip, compress, deflate, br' }
问题分析与解决方案
核心问题
- 拦截对象错误:你当前用的Nest拦截器是处理客户端发给你的入站请求,修改的是这个请求的headers,完全不会影响你的应用发给其他服务的出站请求。
- Token初始化异步未处理:构造函数里调用
trySaveToken是异步请求,但没等它完成就发起业务请求,导致初始token是空字符串。 - 重试逻辑无效:
return of(originalRequest)只是把请求配置对象返回,不会触发Axios重新发送请求。
正确实现方式
要处理出站请求,得用HttpService底层的Axios拦截器,而非Nest的Interceptor。下面是修正后的代码:
1. 重构拦截器(使用Axios拦截器)
@Injectable() export class AuthTokenInterceptor implements OnModuleInit { private readonly authUrl = "https://..."; private token = ""; private isRefreshing = false; private refreshSubscribers: ((token: string) => void)[] = []; constructor(private readonly httpService: HttpService) {} async onModuleInit() { // 先初始化Token,确保后续请求有可用凭证 await this.trySaveToken(); // 给Axios添加请求拦截器,统一处理Authorization头 this.httpService.axiosRef.interceptors.request.use( (config) => { if (this.token && !config.headers.Authorization) { config.headers.Authorization = this.token; } return config; }, (error) => Promise.reject(error) ); // 添加响应拦截器,处理Token过期重试 this.httpService.axiosRef.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config; // 判断是否需要刷新Token并重试 if ( error.response?.status === 401 && originalRequest && originalRequest.url !== this.authUrl && !originalRequest._retry ) { // 防止多个请求同时触发Token刷新 if (this.isRefreshing) { return new Promise((resolve) => { this.refreshSubscribers.push((token) => { originalRequest.headers.Authorization = token; resolve(this.httpService.axiosRef(originalRequest)); }); }); } originalRequest._retry = true; this.isRefreshing = true; try { // 获取新Token await this.trySaveToken(); originalRequest.headers.Authorization = this.token; // 通知所有等待的请求重试 this.refreshSubscribers.forEach((callback) => callback(this.token)); this.refreshSubscribers = []; // 重试原请求 return this.httpService.axiosRef(originalRequest); } catch (refreshError) { this.refreshSubscribers = []; return Promise.reject(refreshError); } finally { this.isRefreshing = false; } } return Promise.reject(error); } ); } // 改成异步方法,确保Token获取完成 private async trySaveToken() { const response = await this.httpService.post(this.authUrl).toPromise(); this.token = 'Bearer ' + response.data.access_token; } }
2. 在模块中注册拦截器
因为这个拦截器实现了OnModuleInit,模块初始化时会自动执行初始化逻辑:
@Module({ providers: [AuthTokenInterceptor], }) export class AppModule {}
关键说明
- Axios拦截器才是处理出站请求的正确方式:Nest的Interceptor管控的是「入站请求→控制器→响应客户端」的流程,和你的应用发出去的请求无关。
- 异步Token初始化:用
async/await等待Token获取完成,避免初始请求拿不到有效Token。 - 防重复刷新:用
isRefreshing标记和订阅队列,避免多个请求同时触发Token刷新接口。 - 正确重试:调用
this.httpService.axiosRef(originalRequest)才是真正重新发起请求,之前返回配置对象的逻辑完全无效。
内容的提问来源于stack exchange,提问作者Chris Barr
相关产品推荐
相关产品推荐

