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

Angular认证场景下,实现令牌刷新拦截器遇到的技术难题

嘿,我明白你不想用额外的“持有类”,只想靠独立拦截器搞定Angular里短生命周期令牌的自动刷新需求——这完全可以实现!我给你一套贴合你场景的方案,全程用独立拦截器+RxJS操作符完成,不需要多余的单例持有类。

实现方案拆解

1. 先完善你的AuthInterceptor(负责令牌注入+提前过期判断)

你的核心拦截器可以直接集成“提前检查令牌是否即将过期”的逻辑,不用依赖外部持有类:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from './auth.service'; // 这个服务只做令牌的存/取/刷新,不是“持有类”

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler) {
    const currentToken = this.authService.getToken();
    
    if (currentToken) {
      // 提前30秒检查令牌是否要过期,避免请求到一半令牌失效
      if (this.isTokenAboutToExpire(currentToken)) {
        // 先刷新令牌,再用新令牌重试请求
        return this.authService.refreshToken().pipe(
          switchMap((newToken) => {
            this.authService.saveToken(newToken);
            // 克隆请求并注入新令牌
            const authRequest = request.clone({
              headers: request.headers.set('Authorization', `Bearer ${newToken}`)
            });
            return next.handle(authRequest);
          }),
          catchError(() => {
            // 刷新失败直接登出
            this.authService.logout();
            return throwError(() => new Error('令牌刷新失败,请重新登录'));
          })
        );
      } else {
        // 令牌有效,直接注入请求头
        const authRequest = request.clone({
          headers: request.headers.set('Authorization', `Bearer ${currentToken}`)
        });
        return next.handle(authRequest);
      }
    }

    // 没有令牌的请求(比如登录)直接放行
    return next.handle(request);
  }

  // 私有辅助方法:判断令牌是否即将过期
  private isTokenAboutToExpire(token: string): boolean {
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expireTime = payload.exp * 1000; // 转成毫秒
    const now = Date.now();
    // 提前30秒触发刷新,留足请求处理时间
    return expireTime - now < 30000;
  }
}

2. 新增RefreshTokenInterceptor(处理全局401兜底)

有时候可能我们的提前判断没覆盖到(比如用户长时间没操作,令牌已经过期),这时候后端会返回401,用这个独立拦截器来兜底处理,还能避免重复刷新:

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

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
  private isRefreshing = false; // 防止多个请求同时触发刷新

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler) {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        // 只处理非登录请求的401错误,避免循环
        if (error.status === 401 && !request.url.includes('/login')) {
          if (!this.isRefreshing) {
            this.isRefreshing = true;
            // 触发令牌刷新
            return this.authService.refreshToken().pipe(
              switchMap((newToken) => {
                this.isRefreshing = false;
                // 用新令牌重试原请求
                const authRequest = request.clone({
                  headers: request.headers.set('Authorization', `Bearer ${newToken}`)
                });
                return next.handle(authRequest);
              }),
              catchError(() => {
                this.isRefreshing = false;
                this.authService.logout();
                return throwError(() => new Error('令牌已过期,请重新登录'));
              })
            );
          } else {
            // 如果已经在刷新,等待刷新完成后再重试
            return this.authService.tokenRefreshed$.pipe(
              switchMap(() => {
                const newToken = this.authService.getToken()!;
                const authRequest = request.clone({
                  headers: request.headers.set('Authorization', `Bearer ${newToken}`)
                });
                return next.handle(authRequest);
              })
            );
          }
        }
        return throwError(() => error);
      })
    );
  }
}

3. 极简AuthService(无持有类,只做令牌基础操作)

这个服务完全不是“持有类”,只负责令牌的存、取、刷新和状态通知,所有状态存在localStorage里:

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

@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly tokenStorageKey = 'auth_token';
  // 用于通知其他请求令牌已刷新
  public tokenRefreshed$ = new BehaviorSubject<void>(undefined);

  constructor(private http: HttpClient) {}

  getToken(): string | null {
    return localStorage.getItem(this.tokenStorageKey);
  }

  saveToken(token: string): void {
    localStorage.setItem(this.tokenStorageKey, token);
    this.tokenRefreshed$.next(); // 通知所有等待的请求
  }

  logout(): void {
    localStorage.removeItem(this.tokenStorageKey);
    // 这里可以加跳转到登录页的逻辑,比如调用Router.navigate(['/login'])
  }

  refreshToken(): Observable<string> {
    // 调用你的后端刷新令牌接口,返回新令牌
    return this.http.post<{ token: string }>('/api/auth/refresh', {}).pipe(
      map((response) => {
        const newToken = response.token;
        this.saveToken(newToken);
        return newToken;
      })
    );
  }
}

4. 注册拦截器(注意顺序!)

在你的AppModule里注册这两个拦截器,一定要让AuthInterceptor先执行,这样才能先注入令牌:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { AppComponent } from './app.component';
import { AuthInterceptor } from './auth.interceptor';
import { RefreshTokenInterceptor } from './refresh-token.interceptor';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: RefreshTokenInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

核心优势

  • 完全无持有类:所有令牌状态都存在localStorage和RxJS主题里,没有多余的单例持有类
  • 双重保障:提前判断令牌过期+全局401兜底,覆盖所有令牌失效场景
  • 避免重复请求:用isRefreshing标志防止多个请求同时触发令牌刷新,减少后端压力

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:59:51