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

Angular刷新token时误带过期access_token至请求头的问题排查

问题:Angular刷新Token时携带过期Access Token导致后端报错

我正在开发PHP后端+Angular前端的应用,登录模块功能正常,但实现refresh_token时遇到问题:

  • 配置了两个HttpInterceptor:JWTInterceptor负责给所有请求加access_token(功能正常),tokenInterceptor捕获过期JWT并刷新Token
  • 调用refresh token接口时,请求头会被带入过期的access_token,后端返回"Expired JWT Token"错误
  • Postman测试确认:不带access_token调用刷新接口正常,携带过期token则触发错误

原JWTInterceptor代码

import {
  HttpEvent,
  HttpHandlerFn,
  HttpInterceptorFn,
  HttpRequest,
} from '@angular/common/http';
import {catchError, Observable, switchMap, throwError} from "rxjs";
import {inject} from "@angular/core";
import {AuthService} from "./services/auth.service";
import {Router} from "@angular/router";

export const JwtInterceptor: HttpInterceptorFn = (req, next) => {

  let authToken = localStorage.getItem('access_token');
  console.log("JwtInterceptor")
  if (authToken) {
    let authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${authToken}`,
      },
    });
    return next(authReq);
  } else {
    return next(req);
  }
}

原tokenInterceptor代码

import {HttpErrorResponse, HttpInterceptorFn} from '@angular/common/http';
import {catchError, switchMap, throwError} from "rxjs";
import {inject} from "@angular/core";
import {AuthService} from "./services/auth.service";

export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
  console.log("TokenInterceptor")
  let authToken = localStorage.getItem('token');
  let authService = inject(AuthService);

  if (authToken) {
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${authToken}`,
      },
    });
  }
  return next(req).pipe(
    catchError((errorData: HttpErrorResponse) => {

      if(errorData.status == 401) {
        console.error("Token Interceptor Error to 401");

        return authService.refreshToken().pipe(
          switchMap(() => {
            console.error("Refresh token done")
            const newAccessToken = localStorage.getItem('access_token');
            // Retry the original request with the new access token
            req = req.clone({
              setHeaders: {
                Authorization: `Bearer ${newAccessToken}`,
              },
            });
            return next(req);
          }),
          catchError((error) => {
            // Handle refresh token error (e.g., redirect to login page)
            console.error('Error handling expired access token:', error);
            return throwError(error);
          })
        )
      }
      throw errorData;
    })
  );
};

问题分析

  1. 拦截器执行冲突:两个拦截器都给请求添加Authorization头,且JwtInterceptor会给refresh_token接口也带上过期的access_token
  2. tokenInterceptor的错误:错误读取了localStorage.getItem('token')(应该是access_token),同时未跳过refresh_token接口的头添加逻辑
  3. 刷新请求未特殊处理:调用refreshToken时没有剥离过期的Authorization头

修复方案

修复后的JwtInterceptor

import {
  HttpEvent,
  HttpHandlerFn,
  HttpInterceptorFn,
  HttpRequest,
} from '@angular/common/http';

export const JwtInterceptor: HttpInterceptorFn = (req, next) => {
  // 跳过刷新token的接口,避免添加过期的access_token
  if (req.url.includes('/refresh-token')) {
    return next(req);
  }

  const authToken = localStorage.getItem('access_token');
  if (authToken) {
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${authToken}`,
      },
    });
    return next(authReq);
  }
  return next(req);
}

修复后的tokenInterceptor

import {HttpErrorResponse, HttpInterceptorFn} from '@angular/common/http';
import {catchError, switchMap, throwError} from "rxjs";
import {inject} from "@angular/core";
import {AuthService} from "./services/auth.service";
import {Router} from "@angular/router";

export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  // 跳过刷新token的接口,避免重复添加头
  if (req.url.includes('/refresh-token')) {
    return next(req);
  }

  const authToken = localStorage.getItem('access_token');
  let authReq = req;
  if (authToken) {
    authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${authToken}`,
      },
    });
  }

  return next(authReq).pipe(
    catchError((errorData: HttpErrorResponse) => {
      // 只处理非刷新接口的401错误,避免循环调用
      if (errorData.status === 401 && !req.url.includes('/refresh-token')) {
        return authService.refreshToken().pipe(
          switchMap(() => {
            const newAccessToken = localStorage.getItem('access_token');
            // 用新token重试原始请求
            const retryReq = req.clone({
              setHeaders: {
                Authorization: `Bearer ${newAccessToken}`,
              },
            });
            return next(retryReq);
          }),
          catchError((error) => {
            // 刷新失败,清除本地token并跳转登录页
            localStorage.removeItem('access_token');
            localStorage.removeItem('refresh_token');
            router.navigate(['/login']);
            return throwError(error);
          })
        );
      }
      return throwError(errorData);
    })
  );
};

关键修改说明

  • 两个拦截器都添加了跳过refresh_token接口的逻辑,确保该请求不带过期的access_token
  • 修正了tokenInterceptor中错误的token读取逻辑
  • 新增了刷新失败后的用户引导(清除token+跳转登录)
  • 增加了防循环判断,避免刷新接口的401错误触发无限刷新

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 04:07:46