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

Angular 17功能拦截器:请求重试与Token刷新实现求助

Angular 15+ 功能型拦截器实现Token刷新与请求重试

原代码核心问题

  1. 未捕获请求返回的401错误:仅提前检查Token过期,未处理后端返回401的场景
  2. 错误使用subscribe:回调内的return next(request)无法传递给拦截器的返回流,导致重试逻辑失效
  3. 缺少并发请求控制:无全局刷新状态,可能触发重复Token刷新请求
  4. 变量未定义:isRefreshing未声明,会引发运行时错误

修正后的完整拦截器代码

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { ILoginResponse } from '../interfaces/iloginresponse';
import { jwtDecode } from 'jwt-decode';
import { catchError, switchMap, tap, finalize, Observable, Subject } from 'rxjs';

// 全局变量控制刷新状态,避免并发请求重复刷新Token
let isRefreshing = false;
let refreshTokenSubject: Subject<string> = new Subject<string>();

export const apiInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  let request = req.clone();
  
  // 为请求添加Authorization头
  const token = sessionStorage.getItem("access_token");
  if (token) {
    request = addAuthHeader(req, token);
    
    // 提前检查Token是否过期(可选,配合后端401处理)
    const tokenData = jwtDecode(token);
    if (tokenData.exp! < Date.now() / 1000 && !isRefreshing) {
      return handleTokenRefresh(req, next, authService);
    }
  }

  // 处理请求返回的错误
  return next(request).pipe(
    catchError((error: HttpErrorResponse) => {
      // 仅处理401未授权错误,且排除刷新Token的请求本身
      if (error.status === 401 && req.url !== '/api/auth/refresh-token') {
        return handleTokenRefresh(req, next, authService);
      }
      // 其他错误直接抛出
      throw error;
    })
  );
};

// 辅助函数:添加Authorization头
function addAuthHeader(req: any, token: string) {
  return req.clone({
    headers: req.headers.set("Authorization", `Bearer ${token}`)
  });
}

// 核心逻辑:处理Token刷新与请求重试
function handleTokenRefresh(req: any, next: any, authService: AuthService): Observable<any> {
  if (!isRefreshing) {
    isRefreshing = true;
    refreshTokenSubject.next('');

    return authService.refreshToken().pipe(
      tap((data: ILoginResponse) => {
        const newToken = data.access_token;
        sessionStorage.setItem("access_token", newToken);
        refreshTokenSubject.next(newToken);
      }),
      switchMap((newTokenData: ILoginResponse) => {
        // 用新Token重试原请求
        return next(addAuthHeader(req, newTokenData.access_token));
      }),
      finalize(() => {
        isRefreshing = false;
      }),
      catchError((error) => {
        // 刷新Token失败,清空存储并跳转登录页
        sessionStorage.removeItem("access_token");
        // 这里可以添加跳转到登录页的逻辑,比如router.navigate(['/login'])
        throw error;
      })
    );
  } else {
    // 已有刷新请求在进行,等待刷新完成后用新Token重试
    return refreshTokenSubject.pipe(
      switchMap((newToken) => {
        return next(addAuthHeader(req, newToken));
      })
    );
  }
}

关键逻辑说明

  1. 全局状态控制:

    • isRefreshing标记当前是否正在刷新Token,避免并发请求重复触发刷新
    • refreshTokenSubject用于在刷新完成后通知等待的请求,统一使用新Token重试
  2. 错误拦截与处理:

    • 通过catchError捕获请求返回的HttpErrorResponse,判断是否为401错误
    • 排除刷新Token的请求本身,避免死循环
  3. Token刷新与重试:

    • 刷新Token成功后,更新本地存储的Token,并通过refreshTokenSubject通知所有等待的请求
    • 使用switchMap切换到重试请求的Observable,确保流的连续性
    • 刷新失败时清空Token并处理登录跳转逻辑
  4. 提前Token过期检查:

    • 可选逻辑:在发送请求前检查Token是否即将过期,提前触发刷新,避免请求先返回401

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 21:29:55