Angular 5 HttpInterceptor请求前刷新Token失效问题排查与修复
Let's break down the problem and fix your code step by step. The core issue here is related to how the Observable from refreshToken() is being handled, plus some minor state management glitches that are blocking subsequent requests.
Key Issues in Your Current Code
- Duplicate
isRefreshingTokenReset: You're settingthis.isRefreshingToken = falseboth inside theswitchMapsuccess handler and in thefinallyblock. This can lead to race conditions where the flag is reset prematurely, causing unexpected behavior with waiting requests. - Incomplete Error Propagation: When the refresh token request fails, waiting requests aren't properly notified of the error, leaving them hanging indefinitely instead of failing gracefully.
- Outdated RxJS Chaining: While Angular 5 uses RxJS 5.x, sticking to pipeable operators makes the code clearer and aligns with modern practices, which also helps avoid subtle Observable subscription issues.
Modified Interceptor Code
Here's the corrected version with detailed explanations:
@Injectable() export class TokenInterceptor implements HttpInterceptor { private refreshTokenSubject = new BehaviorSubject<string | null>(null); private isRefreshingToken = false; constructor(private authService: AuthService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const tokenData = AuthService.getCurrentSessionData(); // If token is already being refreshed, wait for the new token to retry the request if (this.isRefreshingToken) { return this.refreshTokenSubject.pipe( filter(token => token !== null), take(1), switchMap(token => next.handle(this.setAuthHeader(request, token))), catchError(err => throwError(() => err)) ); } // Skip token checks if no access token exists const tokenSet = tokenData?.token; if (!tokenSet) { return next.handle(request); } // Proceed with request if token hasn't expired const tokenExpired = new Date(tokenData.expirationDate) < new Date(); if (!tokenExpired) { return next.handle(this.setAuthHeader(request, tokenData.token)); } // Logout immediately if refresh token is also expired const tokenRefreshable = tokenData.refreshToken && new Date(tokenData.refreshTokenExpirationDate) > new Date(); if (!tokenRefreshable) { this.authService.logout(); return throwError(() => 'Refresh token expired'); } // Start token refresh process this.isRefreshingToken = true; this.refreshTokenSubject.next(null); // Signal waiting requests to hold on return this.authService.refreshToken().pipe( switchMap((res: any) => { // Update session data with new tokens AuthService.storeSessionData(res, Utils.getLocalStorageItem(STORAGE_KEYS.STAY_LOGGED_IN)); this.isRefreshingToken = false; this.refreshTokenSubject.next(res.access_token); // Send new token to waiting requests // Retry the original request with fresh token return next.handle(this.setAuthHeader(request, res.access_token)); }), catchError((err) => { // Handle refresh failure: reset state, logout, and notify waiting requests this.isRefreshingToken = false; this.authService.logout(); this.refreshTokenSubject.error(err); // Propagate error to waiting requests this.refreshTokenSubject.next(null); // Reset subject for future requests return throwError(() => err); }) ); } private setAuthHeader(request: HttpRequest<any>, token: string): HttpRequest<any> { return request.clone({ setHeaders: { Authorization: `Bearer ${token}`, }, }); } }
What Changed & Why
- Cleaned Up State Management: Removed the redundant
finallyblock to avoid resettingisRefreshingTokentwice. Now we only reset the flag once in both success and error handlers, ensuring consistent state. - Proper Error Notification: When the refresh token request fails, we call
refreshTokenSubject.error(err)to notify all waiting requests of the failure, so they don't hang and can handle the error appropriately. - Pipeable Operators: Switched to using
pipe()for RxJS operators (likefilter,take,switchMap) which is more readable and aligns with Angular's recommended practices for RxJS usage. - Meaningful Error Throwing: Replaced
Observable.throw('')withthrowError(() => err)to propagate actual error details, making debugging easier and allowing components to handle specific error cases.
Verify the refreshToken Method
Your existing refreshToken method is correct, but double-check that environment.apiPath is properly configured and your backend accepts the refresh_token payload. Here's the method again for reference:
public refreshToken() { const tokenData = AuthService.getCurrentSessionData(); return this.http.post( `${environment.apiPath}/auth/refresh`, { refresh_token: tokenData.refreshToken }, ); }
With these changes, the refreshToken() request will trigger correctly, and subsequent requests will either retry with the new token if refresh succeeds, or fail immediately (and trigger logout) if refresh fails.
内容的提问来源于stack exchange,提问作者ilinieja

