Angular HttpInterceptor处理Auth0令牌刷新及请求挂起的实现问题
这场景我熟!在Angular里实现带令牌刷新的HttpInterceptor,核心就是要搞定「刷新期间挂起所有请求」这个点,我给你一套完整的实现方案,亲测好用:
核心思路
- 用一个共享的状态标识避免重复发起刷新请求(比如同时多个请求触发令牌过期,只发一次刷新)
- 刷新时把所有后续请求存入“等待队列”,刷新完成后用新令牌统一重试
- 刷新成功则更新本地令牌、重试所有挂起请求;失败则直接执行登出逻辑
完整代码实现
首先是拦截器的代码:
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
相关产品推荐
相关产品推荐

