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

Angular 17+NgRx中刷新令牌拦截器的并发同步问题

问题分析

核心问题在于并发请求触发重复刷新令牌:当多个请求同时检测到令牌过期时,会并发调用refresh-token接口。第一个请求成功刷新后,数据库中的account.Token已更新为新值;后续请求携带旧令牌调用刷新接口时,后端校验("Bearer " + account.Token) != token不匹配,直接抛出「Session Not Found」错误,导致所有后续请求阻塞。

现有拦截器的refreshTokenAndContinue方法每次都会发起新的刷新请求,完全没有做并发控制,这是问题的根源。

解决方案

核心思路是共享刷新令牌的请求流,确保同一时间只有一个refresh-token请求在执行,后续请求等待该请求完成后复用结果,同时优化令牌过期判断逻辑,减少不必要的刷新调用。

修改后的拦截器代码

@Injectable()
export class HttpHeaderRequestInterceptor implements HttpInterceptor {
  // 全局变量:跟踪当前是否有刷新令牌请求在执行
  private refreshTokenInProgress$: Observable<string> | null = null;

  constructor(
    private _accountAPI: AccountAPI,
    private _store: Store,
    private _authUtils: AuthUtils
  ) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const refreshEndpoint = 'refresh-token';
    const excludedEndpoints = ['login', 'register', 'reset-password'];

    if (request.url.includes(refreshEndpoint)) {
      return this.setAuthorizationHeader(request, next);
    } else if (
      excludedEndpoints.some(endpoint => request.url.includes(endpoint)) ||
      !request.url.includes('api')
    ) {
      return next.handle(request);
    } else {
      // 先校验当前令牌是否过期,再决定是否刷新
      return from(this._store.select(selectAuthToken)).pipe(
        take(1),
        switchMap(token => {
          if (token && !this._authUtils.isAuthTokenExpired(token)) {
            // 令牌未过期,直接添加请求头
            return this.addRequestHeaders(request, next, token);
          }
          // 令牌过期,执行带并发控制的刷新逻辑
          return this.refreshTokenIfNeeded().pipe(
            switchMap(newToken => this.addRequestHeaders(request, next, newToken))
          );
        })
      );
    }
  }

  private setAuthorizationHeader(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return from(this._store.select(selectAuthToken)).pipe(
      take(1),
      switchMap(token => {
        const updatedRequest = request.clone({
          setHeaders: { Authorization: `Bearer ${token}` }
        });
        return next.handle(updatedRequest);
      })
    );
  }

  // 统一封装请求头添加逻辑
  private addRequestHeaders(
    request: HttpRequest<any>,
    next: HttpHandler,
    token: string
  ): Observable<HttpEvent<any>> {
    return this._store.select(selectLangState).pipe(
      take(1),
      switchMap(lang => {
        const timeZoneOffset = new Date().getTimezoneOffset().toString();
        const updatedRequest = request.clone({
          setHeaders: {
            Authorization: `Bearer ${token}`,
            'Accept-Language': lang || DEFAULT_LANG,
            'Timezone-Offset': timeZoneOffset
          }
        });
        return next.handle(updatedRequest);
      })
    );
  }

  // 带并发控制的刷新令牌逻辑
  private refreshTokenIfNeeded(): Observable<string> {
    // 如果已有刷新请求在执行,直接复用该流
    if (this.refreshTokenInProgress$) {
      return this.refreshTokenInProgress$;
    }

    // 发起新的刷新请求,并绑定全局跟踪变量
    this.refreshTokenInProgress$ = this._accountAPI.refreshToken().pipe(
      switchMap(tokenResponse => {
        const tokenExists = tokenResponse.headers.get('Authorization');
        const token = tokenExists ? tokenExists.split(' ')[1].trim() : '';
        const tokenExpiration = this._authUtils.getAuthTokenExpirationDate(token);
        
        // 更新Store中的令牌
        this._store.dispatch(
          accountActions.refreshTokenSuccess({ authToken: token, tokenExpiration })
        );

        return of(token);
      }),
      catchError(error => {
        // 刷新失败时清空全局变量,避免后续请求复用错误流
        this.refreshTokenInProgress$ = null;
        throw error;
      }),
      finalize(() => {
        // 请求完成(成功/失败)后清空全局变量
        this.refreshTokenInProgress$ = null;
      })
    );

    return this.refreshTokenInProgress$;
  }
}

关键修改点说明

  1. 并发控制:通过refreshTokenInProgress$全局变量跟踪刷新请求状态,确保同一时间只有一个刷新请求执行,后续请求复用已有流。
  2. 令牌过期校验:先判断当前令牌是否真的过期,避免无意义的刷新调用。
  3. 代码解耦:拆分出addRequestHeaders方法,统一处理请求头添加逻辑,减少重复代码。
  4. 异常处理:在刷新失败时清空全局变量,避免后续请求一直阻塞在错误流上。

后端代码补充说明

后端RefreshToken方法中,当令牌未过期时直接返回原令牌的逻辑是合理的,但前端的并发控制已经避免了重复调用。如果需要进一步优化,可以考虑在返回原令牌时更新account.LastLogin字段,但这不是必须的。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 21:24:52