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

Angular HttpInterceptor处理Auth0令牌刷新及请求挂起的实现问题

这场景我熟!在Angular里实现带令牌刷新的HttpInterceptor,核心就是要搞定「刷新期间挂起所有请求」这个点,我给你一套完整的实现方案,亲测好用:

核心思路

  1. 用一个共享的状态标识避免重复发起刷新请求(比如同时多个请求触发令牌过期,只发一次刷新)
  2. 刷新时把所有后续请求存入“等待队列”,刷新完成后用新令牌统一重试
  3. 刷新成功则更新本地令牌、重试所有挂起请求;失败则直接执行登出逻辑

完整代码实现

首先是拦截器的代码:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class TokenRefreshInterceptor implements HttpInterceptor {
  // 标记是否正在刷新令牌
  private isRefreshing = false;
  // 用于通知所有挂起请求刷新完成的信号量
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const currentToken = this.authService.getAccessToken();

    // 先主动检查令牌是否即将过期(提前30秒刷新,避免请求到一半过期)
    if (currentToken && this.isTokenExpiringSoon(currentToken)) {
      return this.handleTokenRefresh(request, next);
    }

    // 正常请求:给请求带上令牌
    return next.handle(this.addTokenToRequest(request, currentToken)).pipe(
      catchError((error) => {
        // 如果服务器返回401,说明令牌已经过期,触发刷新
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return this.handleTokenRefresh(request, next);
        }
        return throwError(() => error);
      })
    );
  }

  private handleTokenRefresh(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // 如果还没在刷新,就发起刷新请求
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshToken().pipe(
        switchMap((newTokenData) => {
          // 刷新成功:更新状态,通知所有挂起请求
          this.isRefreshing = false;
          this.refreshTokenSubject.next(newTokenData);
          this.authService.setAccessToken(newTokenData.accessToken);
          // 用新令牌重试当前请求
          return next.handle(this.addTokenToRequest(request, newTokenData.accessToken));
        }),
        catchError((error) => {
          // 刷新失败:更新状态,执行登出
          this.isRefreshing = false;
          this.authService.logout();
          return throwError(() => error);
        })
      );
    } else {
      // 正在刷新中:等待刷新完成,拿到新令牌后重试请求
      return this.refreshTokenSubject.pipe(
        filter(tokenData => tokenData !== null),
        take(1),
        switchMap(tokenData => {
          return next.handle(this.addTokenToRequest(request, tokenData.accessToken));
        })
      );
    }
  }

  // 给请求添加Authorization头
  private addTokenToRequest(request: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  // 检查令牌是否即将过期(这里设置提前30秒)
  private isTokenExpiringSoon(token: string): boolean {
    const decodedToken = JSON.parse(atob(token.split('.')[1]));
    const expirationTime = decodedToken.exp * 1000;
    const timeUntilExpire = expirationTime - Date.now();
    return timeUntilExpire < 30000;
  }
}

然后是AuthService里的关键方法(你需要根据自己的后端接口调整):

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(private http: HttpClient, private router: Router) {}

  // 获取本地存储的访问令牌
  getAccessToken(): string {
    return localStorage.getItem('accessToken') || '';
  }

  // 更新本地存储的访问令牌
  setAccessToken(token: string): void {
    localStorage.setItem('accessToken', token);
  }

  // 调用后端刷新令牌接口
  refreshToken(): Observable<any> {
    const refreshToken = localStorage.getItem('refreshToken');
    return this.http.post('/api/auth/refresh', { refreshToken });
  }

  // 登出逻辑:清空令牌+跳转到登录页
  logout(): void {
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    this.router.navigate(['/login']);
  }
}

关键细节解释

  • isRefreshing:防止多个请求同时触发刷新,避免重复调用后端刷新接口
  • refreshTokenSubject:作为一个“通知器”,刷新完成后会把新令牌推送给所有等待中的请求
  • switchMap:用刷新令牌的Observable替换当前请求的Observable,实现“先刷新再重试”的逻辑
  • 双重触发机制:既主动检查令牌即将过期,也处理401错误,覆盖所有令牌过期场景

最后别忘了在app.module.ts里注册这个拦截器:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TokenRefreshInterceptor } from './token-refresh.interceptor';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenRefreshInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

内容的提问来源于stack exchange,提问作者mc.suchecki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 08:24:44