Angular 5 Http拦截器JWT令牌刷新:请求排队问题
解决Angular拦截器中并发401请求的令牌刷新信号量问题
这是个非常常见的场景——当多个请求同时触发401未授权时,我们绝对不想重复发起刷新令牌的请求,而是要让所有后续请求挂起,等待令牌刷新完成后,用新令牌重新发起这些请求。下面是修改后的TokenInterceptor实现,附带详细的逻辑说明:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpInterceptor, HttpEvent } from '@angular/common/http'; import { Observable, Subject, throwError } from 'rxjs'; import { catchError, switchMap, finalize } from 'rxjs/operators'; import { AuthService } from './auth.service'; // 请确保导入路径正确 @Injectable() export class TokenInterceptor implements HttpInterceptor { private auth: AuthService; // 跟踪是否正在刷新令牌 private refreshTokenInProgress = false; // 作为信号量,通知等待的请求刷新已完成 private refreshTokenSubject: Subject<any> = new Subject<any>(); constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { this.auth = this.injector.get(AuthService); // 为请求添加认证头(如果已登录) if(this.auth.isAuthenticated()){ request = request.clone({ setHeaders: { Accept: 'application/json', Authorization: `Bearer ${localStorage.getItem('access_token')}` } }); } else { request = request.clone({ setHeaders: { Accept: 'application/json' } }); } return next.handle(request).pipe( catchError(error => { // 仅处理401未授权错误 if (error.status === 401) { // 排除刷新令牌的请求本身,避免无限循环拦截 if (!request.url.includes('/refresh-token')) { return this.handle401Error(request, next); } } // 其他错误直接抛出 return throwError(() => error); }) ); } private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // 如果没有正在刷新令牌,启动刷新流程 if (!this.refreshTokenInProgress) { this.refreshTokenInProgress = true; // 重置信号量Subject,确保每次刷新都是新的通知 this.refreshTokenSubject = new Subject<any>(); // 调用AuthService的刷新令牌方法 return this.auth.refreshToken().pipe( switchMap((newToken: any) => { // 刷新成功,更新本地存储的access_token localStorage.setItem('access_token', newToken.access_token); // 通知所有等待的请求:刷新完成 this.refreshTokenSubject.next(newToken); // 重发当前请求(用新令牌) return next.handle(this.addAuthHeader(request)); }), catchError((refreshError) => { // 刷新失败,清空认证状态,跳转到登录页 this.auth.logout(); this.refreshTokenSubject.error(refreshError); return throwError(() => refreshError); }), finalize(() => { // 无论成功失败,重置刷新状态 this.refreshTokenInProgress = false; this.refreshTokenSubject.complete(); }) ); } else { // 已有刷新请求在进行,让当前请求等待刷新完成后重试 return this.refreshTokenSubject.asObservable().pipe( switchMap(() => { // 刷新完成,用新令牌重发请求 return next.handle(this.addAuthHeader(request)); }) ); } } // 封装添加认证头的方法,避免重复代码 private addAuthHeader(request: HttpRequest<any>): HttpRequest<any> { return request.clone({ setHeaders: { Accept: 'application/json', Authorization: `Bearer ${localStorage.getItem('access_token')}` } }); } }
关键逻辑说明:
状态跟踪变量:
refreshTokenInProgress:标记当前是否正在执行令牌刷新操作,避免重复发起刷新请求refreshTokenSubject:作为“信号量”,当刷新完成时,通过它通知所有挂起的请求可以重试了
401错误处理分支:
- 首先排除刷新令牌的请求本身,防止拦截器无限循环处理这个请求的401错误
- 如果没有正在刷新,就启动刷新流程:调用
auth.refreshToken(),成功后更新令牌、通知等待的请求,并重发当前请求;失败则清空认证状态并跳转登录 - 如果已经有刷新请求在进行,当前请求就订阅
refreshTokenSubject,等待刷新完成后自动用新令牌重试
代码优化:把添加认证头的逻辑封装成
addAuthHeader方法,减少重复代码,提高可维护性
注意事项:
- 请确保你的
AuthService中已经实现了refreshToken()方法,该方法应该返回一个Observable,发出新的令牌数据 - 如果你的Angular版本使用的是RxJS 6+,请确保使用
pipe操作符来组合操作符(如catchError、switchMap),旧版本的catch已经被废弃 - 可以根据你的业务需求调整刷新失败后的处理逻辑(比如跳转登录页的方式)
内容的提问来源于stack exchange,提问作者Bartando
相关产品推荐
相关产品推荐

