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

Angular错误拦截器多401响应问题:并发请求刷新令牌方案咨询

Angular并发请求下Refresh Token重复触发问题解决方案

问题场景

Access Token过期后刷新页面,同时发起的3个授权请求全部返回401错误。拦截器本应调用刷新接口获取新Token,但因为并发请求的竞态问题:第一个请求刷新Token后更新了本地存储,后续两个请求仍携带旧Token触发401拦截,导致重复执行刷新逻辑。

核心问题

当前拦截器未处理并发请求下的Token刷新冲突,多个请求同时触发401时会重复调用刷新接口,且后续请求无法复用已刷新的新Token。

解决方案思路

  1. 用一个共享Observable标记是否正在刷新Token,避免重复调用刷新接口
  2. 所有触发401的请求订阅这个共享Observable,等Token刷新完成后,统一用新Token重新发起请求
  3. 刷新失败时统一执行登出逻辑

修改后的ErrorInterceptor代码

@Injectable({ providedIn: 'root' })
export class ErrorInterceptor implements HttpInterceptor {
  // 共享的刷新Token请求Observable,控制并发请求
  private refreshTokenRequest$: Observable<IUser | null> | null = null;

  constructor(private readonly authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401 && !req.url.includes('auth/login')) {
          return this.handle401HttpError(req, next);
        }
        // 非401错误直接抛出
        return throwError(() => error);
      })
    );
  }

  private handle401HttpError(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // 已有刷新请求在进行,直接订阅等待结果,避免重复刷新
    if (this.refreshTokenRequest$) {
      return this.refreshTokenRequest$.pipe(
        switchMap(user => {
          if (user) {
            return next.handle(this.addAuthHeader(req, user.token));
          }
          return throwError(() => new Error('刷新Token失败'));
        }),
        catchError(() => {
          this.authService.logout();
          return throwError(() => new Error('刷新Token失败,已登出'));
        })
      );
    }

    // 无正在进行的刷新请求,发起新的刷新请求
    const currentAccessToken = localStorage.getItem('token') as string;
    const currentRefreshToken = localStorage.getItem('refreshToken') as string;

    const request: IRefreshTokenRequest = {
      accessToken: currentAccessToken,
      refreshToken: currentRefreshToken,
    };

    // 保存刷新请求Observable,用finalize确保请求结束后清空标记
    this.refreshTokenRequest$ = this.authService.refreshToken(request).pipe(
      finalize(() => {
        this.refreshTokenRequest$ = null;
      })
    );

    return this.refreshTokenRequest$.pipe(
      switchMap(user => {
        if (user) {
          localStorage.setItem('token', user.token);
          localStorage.setItem('refreshToken', user.refreshToken);
          return next.handle(this.addAuthHeader(req, user.token));
        }
        return throwError(() => new Error('刷新Token失败'));
      }),
      catchError(err => {
        this.authService.logout();
        return throwError(() => err);
      })
    );
  }

  // 辅助方法:为请求添加新的Authorization头
  private addAuthHeader(req: HttpRequest<any>, token: string): HttpRequest<any> {
    return req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
  }
}

关键改动说明

  • 新增refreshTokenRequest$变量标记刷新状态,所有并发401请求共享该Observable,避免重复调用刷新接口
  • 用finalize操作符在刷新请求结束(成功/失败)后清空标记,确保后续需要时能发起新的刷新
  • 移除原代码中错误的.subscribe()调用,改为返回Observable让HTTP客户端处理订阅,避免丢失请求上下文
  • 提取addAuthHeader辅助方法,简化代码逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:50:28