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

Angular应用JWT刷新时如何确保后续HTTP请求等待刷新完成?

嘿,这个场景我太熟悉了——之前做Angular项目时也踩过全局标志的坑,虽然能凑合用,但确实有更优雅的RxJS+Angular原生方案,完美解决刷新时的请求竞态问题,而且代码更可维护。核心思路是利用RxJS的Observable共享机制+Angular HTTP拦截器,让所有需要等待刷新的请求自动复用同一个刷新请求,彻底避免旧Token的问题。

第一步:用AuthService管理Token状态与刷新请求

我们可以用BehaviorSubject维护当前Token的状态,同时用一个变量保存正在进行的刷新请求Observable,这样所有后续请求都能订阅同一个刷新流,不会重复发起刷新:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap, finalize, catchError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthService {
  // 保存当前Token的可观察对象,初始值从localStorage取
  private jwtToken$ = new BehaviorSubject<string>(localStorage.getItem('jwt') || '');
  // 保存正在进行的刷新请求,避免重复刷新
  private ongoingRefresh$: Observable<string> | null = null;

  // 对外暴露Token的只读Observable
  get currentToken$(): Observable<string> {
    return this.jwtToken$.asObservable();
  }

  constructor(private http: HttpClient) {}

  renewJwt(): Observable<string> {
    // 如果已经在刷新,直接返回正在进行的请求,避免重复调用接口
    if (this.ongoingRefresh$) {
      return this.ongoingRefresh$;
    }

    // 发起刷新Token的请求
    this.ongoingRefresh$ = this.http.post<{ token: string }>('/api/auth/refresh', {})
      .pipe(
        // 刷新成功后更新Token状态并保存到localStorage
        tap((response) => {
          const newToken = response.token;
          localStorage.setItem('jwt', newToken);
          this.jwtToken$.next(newToken);
        }),
        // 无论成功失败,都清空正在刷新的标志
        finalize(() => {
          this.ongoingRefresh$ = null;
        }),
        // 处理刷新失败的情况,确保标志被清空
        catchError((error) => {
          this.ongoingRefresh$ = null;
          throw error; // 把错误抛出去,让拦截器处理
        })
      );

    return this.ongoingRefresh$;
  }
}

第二步:用HTTP拦截器统一处理请求

Angular的HTTP拦截器是处理全局请求逻辑的最佳场所,我们可以在这里拦截所有请求,判断是否需要等待Token刷新,再用新Token发起请求:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable, switchMap } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return this.authService.currentToken$.pipe(
      switchMap((token) => {
        // 先判断Token是否即将过期(这里需要你自己实现过期判断逻辑)
        if (this.isTokenAboutToExpire(token)) {
          // 如果即将过期,先执行刷新,拿到新Token后再发起请求
          return this.authService.renewJwt().pipe(
            switchMap((newToken) => {
              return this.addTokenToRequest(request, newToken, next);
            })
          );
        } else if (this.authService['ongoingRefresh$']) {
          // 如果已经有刷新请求在进行,等待刷新完成后用新Token发起请求
          return this.authService.renewJwt().pipe(
            switchMap((newToken) => {
              return this.addTokenToRequest(request, newToken, next);
            })
          );
        } else {
          // Token有效且无刷新中,直接添加Token发起请求
          return this.addTokenToRequest(request, token, next);
        }
      })
    );
  }

  // 辅助方法:给请求添加Token并传递给下一个拦截器/后端
  private addTokenToRequest(
    request: HttpRequest<unknown>,
    token: string,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    if (!token) return next.handle(request);
    
    const authRequest = request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next.handle(authRequest);
  }

  // 实现Token过期判断逻辑:解析Token的exp字段,判断是否在阈值内即将过期
  private isTokenAboutToExpire(token: string): boolean {
    if (!token) return false;
    
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const expireTimestamp = payload.exp * 1000;
      // 比如设置为剩余30秒过期时触发刷新,你可以根据自己的刷新间隔调整
      return expireTimestamp - Date.now() < 30000;
    } catch (e) {
      return false; // 解析失败则认为Token无效
    }
  }
}

为什么这个方案比全局标志更好?

  • 避免竞态问题:所有需要等待刷新的请求都会订阅同一个刷新Observable,确保只有一个刷新请求在进行,不会出现多个请求同时触发刷新的情况。
  • 自动同步Token:刷新完成后,BehaviorSubject会自动通知所有订阅者更新Token,后续请求都会用新Token,完全不会用到旧Token。
  • 全局统一处理:不需要在每个组件或服务里手动判断Token状态,拦截器会自动处理所有请求,代码更简洁、可维护。

额外注意点

  • 刷新失败的处理:一定要在renewJwt里用finalize和catchError清空ongoingRefresh$,否则刷新失败后这个变量会一直存在,导致后续请求一直等待。
  • 过期阈值调整:isTokenAboutToExpire的时间阈值要和你的自动刷新间隔匹配,比如你每2分钟刷新一次,可以设置剩余1分钟时触发刷新,避免刚好在请求发起时Token过期。
  • 无操作场景:如果用户长时间不操作,浏览器可能会暂停定时器,导致自动刷新失效,可以结合路由守卫或document.visibilityState来在用户回到页面时检查Token状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:52:42