Angular刷新Token后重放请求,原请求Subscribe未触发问题
问题分析与解决思路
核心问题
你的拦截器返回的Observable未正确完成,导致forkJoin无法触发订阅回调——forkJoin要求所有内部Observable都完成才会发出结果,而当前代码中tokenSubject是持续活跃的Subject,即使重放的请求完成,外层Observable仍因tokenSubject未完成而保持活跃状态。同时,多请求并发401时,后续请求可能错过tokenSubject的next信号,导致一直阻塞。
具体修复步骤
1. 替换Subject为ReplaySubject,确保后续请求能获取刷新信号
普通Subject的订阅者只能收到订阅后的事件,改用ReplaySubject(1)可以缓存最近1次事件,让晚订阅的请求也能拿到刷新完成的信号:
isRefreshingToken: boolean = false tokenSubject: ReplaySubject<void> = new ReplaySubject<void>(1) // 替换为ReplaySubject
2. 限制tokenSubject仅取一次信号,确保Observable能完成
在使用tokenSubject时添加take(1)操作符,确保取到刷新信号后,Observable能正常完成,满足forkJoin的要求:
return this.tokenSubject.pipe( take(1), // 仅取一次信号,之后Observable完成 switchMap(() => next.handle(this.tokenRequest(req))) )
3. 处理用户取消登录的场景
如果用户关闭登录对话框不登录,需要主动抛出错误并重置状态,避免请求一直阻塞:
this.dialog.open(LoginDialog).afterClosed().subscribe((loginSuccess) => { this.isRefreshingToken = false if (loginSuccess) { this.tokenSubject.next() } else { // 用户取消登录,抛出错误并清空Subject this.tokenSubject.error(new Error('用户取消登录')) this.tokenSubject = new ReplaySubject<void>(1) // 重置Subject,避免影响后续请求 } })
4. 确保请求克隆的正确性
确认tokenRequest方法中能正确获取最新的Token,避免重放请求时使用旧Token:
tokenRequest(req: HttpRequest<any>): HttpRequest<any> { const token = this.authService.getLatestToken() // 确保这里拿到的是刷新后的新Token return req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }); }
完整修复后的拦截器代码示例
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpInterceptor, HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { Observable, ReplaySubject, throwError } from 'rxjs'; import { catchError, switchMap, take } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { LoginDialog } from './login-dialog.component'; import { AuthService } from './auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { isRefreshingToken: boolean = false; tokenSubject: ReplaySubject<void> = new ReplaySubject<void>(1); constructor(private dialog: MatDialog, private authService: AuthService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> { return next.handle(this.tokenRequest(req)).pipe( catchError((error) => { if (error instanceof HttpErrorResponse && error.status === HttpStatusCode.Unauthorized) { if (!this.isRefreshingToken) { this.isRefreshingToken = true; this.dialog.open(LoginDialog).afterClosed().subscribe((loginSuccess) => { this.isRefreshingToken = false; if (loginSuccess) { this.tokenSubject.next(); } else { this.tokenSubject.error(new Error('用户取消登录')); this.tokenSubject = new ReplaySubject<void>(1); } }); } return this.tokenSubject.pipe( take(1), switchMap(() => next.handle(this.tokenRequest(req))) ); } return throwError(() => error); }) ); } tokenRequest(req: HttpRequest<any>): HttpRequest<any> { const token = this.authService.getLatestToken(); return req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }); } }
修复逻辑说明
ReplaySubject解决了并发请求中晚订阅的请求无法获取刷新信号的问题;take(1)确保重放请求完成后,整个Observable能正常结束,让forkJoin收到所有完成信号;- 处理取消登录的场景,避免请求无限期阻塞;
- 确保重放请求使用最新Token,避免再次触发401。
内容的提问来源于stack exchange,提问作者lostintranslation
相关产品推荐
相关产品推荐

