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

Angular拦截器刷新令牌时请求重复3次问题求助

Angular Token刷新拦截器重复请求问题解决

问题描述

我实现了一个用于刷新Access Token的Angular拦截器,无需刷新令牌时一切正常;但触发刷新操作时,恰好经过该拦截器的请求会重复执行3次,同时刷新请求本身也会运行3次。我已经移除了refresh方法里的retry(2),但问题还是没解决。

原拦截器代码

export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
  let auth = inject(AuthService);
  if (!auth.loggedIn() || !auth.AccessExpired() || req.headers.has('refresh_token') || req.headers.has("no-intercept")) {
    return next(req);
  }
  return auth.refresh().pipe(
    catchError(() => next(req)),
    switchMap((res) => {
      var response = res as RefreshResponse;
      const newReq = req.clone({
        headers: req.headers.set("Authorization", `Bearer ${response.access_token}`)
      })
      return next(newReq);
    })
  )
};

原refresh方法代码

refresh() {
  if (this.loggedIn()) {
    return this.http.post<RefreshResponse>(this.baseUrl + "refresh", {}, {
      headers: new HttpHeaders().set("refresh_token", this.refreshToken ?? "")
    }).pipe(
      tap(
        res => {
          console.log('Responded ' + res.access_token);
          this.accessToken = res.access_token
        }
      ),
      retry(2),
      catchError((err) => {
        console.log("errored" + err)
        this.logout();
        this.noti.showError("Your session expired, log in again.", 5000);
        return throwError(() => new Error(err));
      })
    )
  }
  return throwError(() => "User is not logged in, no refresh possible");  
}

问题根源

核心问题是并发请求触发了多次刷新操作:当多个请求同时进入拦截器,且都检测到Token过期时,每个请求都会独立调用auth.refresh(),导致重复发起刷新API;同时每个刷新完成后,又会重新发起原请求,最终造成请求重复执行。

解决方案

在AuthService中维护一个共享的刷新请求Observable,确保同一时间只有一个刷新请求在执行,所有需要等待刷新的请求复用同一个刷新结果。

修改后的AuthService代码

private refreshRequest$: Observable<RefreshResponse> | null = null;

refresh() {
  if (!this.loggedIn()) {
    return throwError(() => "用户未登录,无法刷新令牌");
  }

  // 如果已有刷新请求在进行,直接复用这个请求流
  if (this.refreshRequest$) {
    return this.refreshRequest$;
  }

  this.refreshRequest$ = this.http.post<RefreshResponse>(`${this.baseUrl}refresh`, {}, {
    headers: new HttpHeaders().set("refresh_token", this.refreshToken ?? "")
  }).pipe(
    tap(res => {
      console.log('刷新成功,新Token:' + res.access_token);
      this.accessToken = res.access_token;
    }),
    // 请求完成(成功/失败)后清空缓存的请求对象
    finalize(() => {
      this.refreshRequest$ = null;
    }),
    catchError((err) => {
      console.log("刷新失败:" + err);
      this.logout();
      this.noti.showError("会话已过期,请重新登录", 5000);
      return throwError(() => new Error(err));
    })
  );

  return this.refreshRequest$;
}

修改后的拦截器代码

export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  
  // 跳过拦截的场景:未登录、Token未过期、携带refresh_token头、携带no-intercept头
  if (!auth.loggedIn() || !auth.AccessExpired() || req.headers.has('refresh_token') || req.headers.has("no-intercept")) {
    return next(req);
  }

  return auth.refresh().pipe(
    // 刷新成功后,用新Token克隆并发起请求
    switchMap((res) => {
      const newReq = req.clone({
        headers: req.headers.set("Authorization", `Bearer ${res.access_token}`)
      });
      return next(newReq);
    }),
    // 刷新失败时,直接发起原请求(可根据业务调整逻辑)
    catchError(() => next(req))
  );
};

关键修改点

  1. 添加刷新请求缓存:用refreshRequest$变量缓存正在进行的刷新请求,多个并发请求进来时复用同一个Observable,避免重复调用刷新API。
  2. 自动清理缓存:通过finalize操作符在请求完成后清空缓存,确保后续需要刷新时能正常发起新请求。
  3. 调整拦截器逻辑:将catchError移到switchMap之后,逻辑更清晰,避免不必要的请求重复触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 20:52:42