You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中遇401错误时如何排队HTTP请求并自动重试?

Angular JWT令牌刷新:并行请求排队重试解决方案

你的核心问题是多个并行请求(如forkJoin发起的请求)遇到401时,无法统一等待令牌刷新完成后重试。以下是针对你现有拦截器的优化方案,解决并行请求的排队问题:

关键问题分析

  1. 初始请求未自动带令牌:现有拦截器仅在重试时添加令牌,导致所有请求默认无令牌发送,触发不必要的401错误。
  2. 并行请求重复触发刷新:原逻辑虽用isRefreshingToken标记,但未统一管理刷新请求,可能导致潜在的重复刷新(或等待逻辑失效)。
  3. forkJoin错误传播:若初始请求的401未被拦截器正确转换为重试Observable,forkJoin会因单个错误终止所有请求。

修改后的完整拦截器代码

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, BehaviorSubject, throwError, filter, take, switchMap, map } from 'rxjs';
import { Router } from '@angular/router';
import { LoggerService } from './logger.service'; // 替换为你的实际服务路径
import { AuthenticationService } from './authentication.service'; // 替换为你的实际服务路径
import { StatusCodes } from 'http-status-codes';
import { appSettings } from '../app-settings'; // 替换为你的实际配置
import { environment } from '../environments/environment'; // 替换为你的实际环境配置
import { IResponse, IAuthResult } from './models'; // 替换为你的实际模型

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  constructor(
    private _router: Router,
    private _logger: LoggerService,
    private _authService: AuthenticationService
  ) {}

  private isRefreshingToken = false;
  private tokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null);
  private refreshTokenObservable: Observable<string | null> | null = null;

  public intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<IResponse>> {
    // 1. 请求发送前自动添加当前有效的access token
    const currentToken = this._authService.getAccessToken();
    const authRequest = currentToken 
      ? this.addTokenInHeader(request, currentToken) 
      : request;

    return next.handle(authRequest).pipe(
      timeout(appSettings.ajaxTimeout),
      catchError((error) => this.errorHandler(error, request, next))
    );
  }
  
  private errorHandler(
    error: HttpErrorResponse,
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<IResponse>> {
    if (error.error instanceof ErrorEvent) {
      if (!environment.production) {
        this._logger.log('客户端/网络错误: ' + error);
      }
    } else {
      const httpErrorCode = error.status;
      switch (httpErrorCode) {
        case StatusCodes.INTERNAL_SERVER_ERROR:
          this._router.navigate(['/internal-server-error']);
          break;
        case StatusCodes.UNAUTHORIZED:
          return this.handle401Error(request, next);
        default:
          this._logger.log('请求错误: ' + error);
          break;
      }
    }

    return throwError(() => error.error || error);
  }

  private handle401Error(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // 2. 仅发起一次令牌刷新请求,所有并行请求等待同一结果
    if (!this.isRefreshingToken) {
      this.isRefreshingToken = true;
      this.tokenSubject.next(null);

      // 存储当前刷新请求,供后续请求订阅
      this.refreshTokenObservable = this._authService.regenerateTokens().pipe(
        map((apiResult) => {
          const authData = apiResult.dataset as IAuthResult;
          this._authService.updateRefreshedTokens(authData);
          const newToken = authData.tokens.access_token;
          this.tokenSubject.next(newToken);
          return newToken;
        }),
        catchError((error) => {
          // 刷新失败:登出并清空令牌
          this._authService.logout();
          this._router.navigate(['/']);
          this.tokenSubject.next(null);
          return throwError(() => error);
        }),
        finalize(() => {
          this.isRefreshingToken = false;
          this.refreshTokenObservable = null;
        })
      );
    }

    // 3. 所有等待的请求订阅同一刷新结果,拿到新令牌后重试
    return this.refreshTokenObservable.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap((token) => {
        return next.handle(this.addTokenInHeader(request, token));
      })
    );
  }

  private addTokenInHeader(
    request: HttpRequest<any>,
    token: string | null
  ): HttpRequest<any> {
    return request.clone({
      setHeaders: { Authorization: 'Bearer ' + token }
    });
  }
}

核心优化点说明

  1. 请求前置令牌添加:在intercept方法中自动为所有请求添加当前有效的access token,减少不必要的401触发。
  2. 统一刷新请求管理:用refreshTokenObservable存储当前的刷新请求,确保所有并行401请求都等待同一刷新操作完成,避免重复刷新。
  3. 并行请求排队重试:后续请求通过订阅同一个刷新Observable,等待新令牌生成后自动重试,确保forkJoin能获取所有请求的重试结果。

配套要求

确保你的AuthenticationService实现以下方法:

  • getAccessToken(): 返回当前有效的access token(从内存/localStorage获取)
  • regenerateTokens(): 发起刷新令牌的API请求,返回包含新令牌的Observable
  • updateRefreshedTokens(authData: IAuthResult): 更新本地存储的令牌信息
  • logout(): 执行登出逻辑,清空令牌并跳转登录页

内容的提问来源于stack exchange,提问作者RAHUL KUNDU

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 03:52:01