Angular拦截器技术问题:处理源请求前通过Observable刷新Token
Angular拦截器中提前刷新Token并串联请求的实现方案
需求背景
在Angular应用中,为避免向后端发送大量携带无效Token的请求,需要在每个请求发起前主动检查Token有效性:
- Token有效:直接转发请求
- Token无效:先调用刷新Token的后端接口(返回Observable),刷新成功后再发送原请求
- 要求提前主动刷新,而非等请求失败后再处理;同时保证同一时刻只有一个刷新请求,后续请求等待刷新完成后复用新Token
用户尝试实现时遇到Observable串联的类型匹配问题,现有代码如下:
export class AuthHeaderInterceptor implements HttpInterceptor { constructor() {} static authenticationOngoing = false; static authenticationObservable: Observable<boolean>; public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { /** * check if the token is valid. * If valid send request * If invalid call "refreshAuthentication()", wait for result, and send request based on result... */ if (!this._isAuthenticationRequest(request) && !this.checkToken()) { if (!AuthHeaderInterceptor.authenticationOngoing) { // send request and wait for response AuthHeaderInterceptor.authenticationOngoing = true; AuthHeaderInterceptor.authenticationObservable = this.refreshAuthentication(); AuthHeaderInterceptor.authenticationObservable.subscribe(() => { AuthHeaderInterceptor.authenticationOngoing = false; // submit source request or reroute if invalid }); } else { AuthHeaderInterceptor.authenticationObservable.subscribe(() => { // submit source request or reroute if invalid }); } } return next.handle(request); } private checkToken() { return this._configStore.getToken().getEndtime() > new Date().getTime() + 10000; } private _isAuthenticationRequest(request): boolean { return request.url.endsWith('/api/authentication'); } private refreshAuthentication(): Observable<boolean> { // returns Observable (as it's doing another request). Value represents if its successfully. } }
修正后的实现方案
核心是利用RxJS的switchMap、shareReplay等操作符处理Observable串联,同时通过静态变量控制并发刷新请求,解决类型匹配问题:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, Observable } from '@angular/common/http'; import { switchMap, catchError, shareReplay, finalize } from 'rxjs/operators'; import { of, throwError } from 'rxjs'; @Injectable() export class AuthHeaderInterceptor implements HttpInterceptor { // 静态变量存储当前正在进行的刷新请求,避免重复发起 static ongoingRefresh$: Observable<boolean> | null = null; constructor() {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // 跳过刷新Token的请求本身,避免循环拦截 if (this._isAuthenticationRequest(request)) { return next.handle(request); } // Token有效,直接添加认证头后转发请求 if (this.checkToken()) { return next.handle(this._addAuthHeader(request)); } // 无正在进行的刷新请求,发起新的刷新请求 if (!AuthHeaderInterceptor.ongoingRefresh$) { AuthHeaderInterceptor.ongoingRefresh$ = this.refreshAuthentication().pipe( // 让多个订阅者共享同一刷新结果,避免重复处理 shareReplay(1), // 刷新请求完成(成功/失败)后清空静态变量 finalize(() => { AuthHeaderInterceptor.ongoingRefresh$ = null; }) ); } // 等待刷新完成后,根据结果处理原请求 return AuthHeaderInterceptor.ongoingRefresh$.pipe( switchMap((refreshSuccess) => { if (refreshSuccess) { // 刷新成功,用新Token克隆并发送原请求 return next.handle(this._addAuthHeader(request)); } else { // 刷新失败,这里可跳转到登录页或抛出错误 return throwError(() => new Error('Token刷新失败')); } }), catchError((error) => { // 刷新请求本身出错,清空静态变量并抛出错误 AuthHeaderInterceptor.ongoingRefresh$ = null; return throwError(() => error); }) ); } private checkToken(): boolean { // 检查Token有效期,预留10秒缓冲时间 const token = this._configStore.getToken(); return token?.getEndtime() > new Date().getTime() + 10000; } private _isAuthenticationRequest(request: HttpRequest<any>): boolean { return request.url.endsWith('/api/authentication'); } private refreshAuthentication(): Observable<boolean> { // 替换为实际的刷新Token请求逻辑,返回Observable<boolean> // 示例: // return this.authService.refreshToken().pipe( // map(() => true), // catchError(() => of(false)) // ); return of(true); // 模拟刷新成功,实际项目替换为真实请求 } private _addAuthHeader(request: HttpRequest<any>): HttpRequest<any> { // 从store获取新Token,添加到请求头 const token = this._configStore.getToken()?.getAccessToken(); return request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } }
关键实现说明
- 并发控制:通过静态变量
ongoingRefresh$记录当前刷新请求,避免同一时刻发起多个刷新请求 - Observable串联:用
switchMap将刷新请求与原请求串联,确保等待刷新完成后再发送原请求 - 结果共享:
shareReplay(1)让多个等待的请求共享同一刷新结果,避免重复处理 - 请求克隆:刷新成功后克隆原请求并添加新认证头,保证请求携带有效Token
- 错误处理:通过
catchError和finalize处理刷新失败场景,及时清空静态变量避免影响后续请求
内容的提问来源于stack exchange,提问作者Veso Alex
相关产品推荐
相关产品推荐

