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

Angular拦截器刷新Token仍触发401错误求助

Access Token过期自动刷新仍出现401错误排查与解决

我尝试在access token过期时自动刷新token,但仍然出现401错误。为测试需求,我将access token过期时间设为1分钟,refresh token设为30天。当access token过期后刷新页面,仍出现401错误,不清楚问题原因。

AuthInterceptor拦截器代码

export class AuthInterceptor implements HttpInterceptor {
  private isRefresh = false;
  constructor(private tokenService:TokenService,
              private authenticationService:AuthenticationService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    let accessToken = this.tokenService.getAccessToken();
    if (accessToken){
      request = this.setHeaders(request, accessToken);
    }
    return next.handle(request).pipe(
      catchError((err) =>{
        console.log(err.status);
        console.log(err instanceof HttpErrorResponse)
        if(err.status === 401 && err instanceof HttpErrorResponse){
          console.log("CALLING!!!!!!!!!!!!!")
          return this.handleAuthorizationError(request, next);
        }
        return throwError(()=>err.error.message);
      })
    );
  }

  private setHeaders(request:HttpRequest<any>, token:string){
    return request.clone({
      setHeaders: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });
  }

  private handleAuthorizationError(request:HttpRequest<any>, next:HttpHandler){
    console.log("CALLING HANDLE")
    if(!this.isRefresh){
      this.isRefresh = true;
      return this.authenticationService.refreshToken(this.tokenService.getRefreshToken()).pipe(
        switchMap((res)=>{
          this.isRefresh = false;
          this.tokenService.setAccessToken(res.accessToken);
          this.tokenService.setRefreshToken(res.refreshToken);
          return next.handle(this.setHeaders(request, res.accessToken));
        })
      )
    }else{
      return next.handle(request);
    }
  }
}

TokenService代码

export class TokenService {
  constructor() { }

  
  getAccessToken(){
    if(localStorage.getItem("accessToken") == null){
      return "";
    }
    return localStorage.getItem("accessToken");
  }

  getRefreshToken(){
    if(localStorage.getItem("refreshToken") == null){
      return "";
    }
    return localStorage.getItem("refreshToken");
  }

  setAccessToken(token:string){
    localStorage.setItem("accessToken", token);
  }

  setRefreshToken(token:string){
    localStorage.setItem("refreshToken", token);
  }

  clearToken(){
    localStorage.removeItem("accessToken");
    localStorage.removeItem("refreshToken");
  }
}

AuthenticationService代码

export class AuthenticationService {
  constructor(private http:HttpClient) { }

  public authenticate(request:AuthenticationRequest){
    return this.http.post<AuthenticationResponse>(`${AUTH_API}/auth/authenticate`, request)
      .pipe(
        catchError((err)=>{
          console.log("Error handled by Service..." + err.status)
          return throwError(()=> new Error(err.error.message));
        })
      );
  }

  public logout(){
    return this.http.get(`${AUTH_API}/auth/logout`);
  }

  public currentUserAccess():Observable<User>{
    return this.http.get<User>(`${AUTH_API}/auth/current-user`)
      .pipe(
        catchError((err) => {
          console.log("Error handled by Service..." + err.status)
          return throwError(()=> new Error("fix later!"));
        })
      );
  }

  public refreshToken(token:any):Observable<AuthenticationResponse>{
    return this.http.post<AuthenticationResponse>(`${AUTH_API}/auth/refresh-token`, {
      "refreshToken": token
    }).pipe(catchError((err)=>{
      console.log("Error handled by Service..." + err.status)
      return throwError(()=> new Error(err.error.message));
    }));
  }
}

请求与响应接口定义

export interface AuthenticationRequest{
  username:string;
  password:string;
}

export interface AuthenticationResponse{
  accessToken:string,
  refreshToken:string
}

问题排查与解决方案

1. 刷新Token请求被拦截带上过期Token

页面刷新后,Interceptor会直接读取localStorage中已过期的accessToken并添加到所有请求头,包括刷新Token的请求本身,导致刷新请求也返回401。

修复代码:
在Interceptor的intercept方法中,判断请求是否为刷新Token接口,跳过Authorization头添加:

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
  let accessToken = this.tokenService.getAccessToken();
  // 跳过刷新Token的请求,避免携带过期Token
  if (accessToken && !request.url.includes('/auth/refresh-token')){
    request = this.setHeaders(request, accessToken);
  }
  // 后续逻辑保持不变
}

2. 刷新失败后未重置状态与清理Token

当前代码中如果刷新Token请求失败,isRefresh标志位不会重置,导致后续所有请求都无法触发刷新逻辑,也没有清理无效Token跳转到登录页的处理。

修复代码:
在handleAuthorizationError的刷新逻辑中添加错误处理:

return this.authenticationService.refreshToken(this.tokenService.getRefreshToken()).pipe(
  switchMap((res)=>{
    this.isRefresh = false;
    this.tokenService.setAccessToken(res.accessToken);
    this.tokenService.setRefreshToken(res.refreshToken);
    return next.handle(this.setHeaders(request, res.accessToken));
  }),
  catchError((refreshErr) => {
    this.isRefresh = false;
    this.tokenService.clearToken();
    // 此处可添加跳转登录页的逻辑
    return throwError(()=> refreshErr);
  })
)

3. 并发请求未等待刷新完成

页面刷新后若有多个并发请求,第一个请求触发401并开始刷新Token,后续请求会直接携带过期Token发送,导致多个401错误。

修复代码:
使用BehaviorSubject统一管理刷新状态,让所有等待的请求复用同一个刷新结果:

export class AuthInterceptor implements HttpInterceptor {
  private isRefresh = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null); // 新增

  constructor(private tokenService:TokenService,
              private authenticationService:AuthenticationService) {}

  // 省略intercept方法

  private handleAuthorizationError(request:HttpRequest<any>, next:HttpHandler){
    if(!this.isRefresh){
      this.isRefresh = true;
      this.refreshTokenSubject.next(null);
      return this.authenticationService.refreshToken(this.tokenService.getRefreshToken()).pipe(
        switchMap((res)=>{
          this.isRefresh = false;
          this.tokenService.setAccessToken(res.accessToken);
          this.tokenService.setRefreshToken(res.refreshToken);
          this.refreshTokenSubject.next(res.accessToken);
          return next.handle(this.setHeaders(request, res.accessToken));
        }),
        catchError((refreshErr) => {
          this.isRefresh = false;
          this.tokenService.clearToken();
          this.refreshTokenSubject.next(null);
          return throwError(()=> refreshErr);
        })
      )
    }else{
      // 等待刷新完成后用新Token重新发送请求
      return this.refreshTokenSubject.pipe(
        filter(token => token !== null),
        take(1),
        switchMap(token => {
          return next.handle(this.setHeaders(request, token));
        })
      );
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 01:54:57