我创建了一个拦截器来确保每个请求都发送
HttpOnly
JWT/refresh
令牌。我试图通过捕获 401 错误并请求服务器刷新来刷新我短暂的 JWT
。它似乎正在发挥作用,尽管可能并不像我预期的那样。
这是我的代码:
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const router: Router = inject(Router);
const authService: AuthService = inject(AuthService);
req = req.clone({
withCredentials: true,
});
return next(req).pipe(
catchError((err: any) => {
if (err instanceof HttpErrorResponse) {
// Handle HTTP errors
if (err.status === 401) {
// handle unauthorized errors
console.error('Unauthorized request:', err);
// retry the request
authService.refreshToken().subscribe({
next: () => {
authService.isRefreshed$.next(true);
},
error: () => {
// Redirect to login page
authService.logout(true);
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: router.url },
});
},
});
return authService.isRefreshed$.pipe(switchMap(() => next(req)));
} else {
// Handle other HTTP error codes
console.error('HTTP error:', err);
}
} else {
// Handle non-HTTP errors
console.error('An error occurred:', err);
}
// Re-throw the error to propagate it further
return throwError(() => err);
})
);
};
当我通过开发者工具查看网络调用时,它们似乎是按照我期望的顺序进行的。 cookie 已成功刷新,但未在重试请求中使用。
我无法通过 TS 使用 HttpOnly cookie 修改请求,所以我不确定如何继续。
目标是使用刷新的 cookie 重试失败的请求。将来我会考虑缓存多个请求并在队列中重试。
更新1:
根据 Naren 的建议进行的更改:
const handle401Error = (
isRefreshing: boolean,
req: HttpRequest<any>,
next: HttpHandlerFn
) => {
if (!isRefreshing) {
isRefreshing = true;
authService.refreshToken().pipe(
switchMap(() => {
isRefreshing = false;
console.log('Token refreshed successfully.');
return next(req);
}),
catchError((err) => {
isRefreshing = false;
console.error('Error refreshing token:', err);
authService.logout();
router.navigate(['/login'], { queryParams: { returnUrl: req.url } });
return throwError(() => err);
})
);
}
return next(req);
};
我在
if (err.status == 401)
时调用handle401error
但上面的代码从未进行刷新 API 调用。我怀疑调用被取消,因为 handle401error 末尾的 return next(req)
正在取消刷新请求。 authService.refreshToken()
有一个日志指令,所以我知道它正在被输入。但是在handle401error中switchMap()和catchError()都没有被执行。
您代码中的问题是订阅中的代码是
asynchoronous
,而其下面的代码是synchoronous
,因此底部代码不会等待主题具有值。
而是尝试下面的方法,他们使用
switchMap
来刷新单个流中的令牌(这消除了这个问题,因为 switchMap 将使流等待可观察到的刷新)在流的末尾,我们再次调用next(req)
重新启动 API 调用或在刷新令牌失败时抛出错误。
确保仅在用户登录时调用刷新令牌
this.storageService.isLoggedIn()
,如果未经授权,则无需刷新逻辑。
您可以使用
bezkoder
中的本教程,指导您如何添加 refreshToken
逻辑。
带有拦截器和 JWT 的 Angular 12 刷新令牌示例(从链接中选择适当的 Angular 版本)
Bezkoder Github - http.interceptor.ts
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const router: Router = inject(Router);
const authService: AuthService = inject(AuthService);
let isRefreshing = false;
req = req.clone({
withCredentials: true,
});
const handle401Error = (isRefreshing: boolean, req: HttpRequest<any>, next: HttpHandler) => {
if (!isRefreshing) { // <- changed here!!!
isRefreshing = true;
if (this.storageService.isLoggedIn()) {
return this.authService.refreshToken().pipe(
switchMap(() => {
isRefreshing = false;
return next(req);
}),
catchError((error) => {
isRefreshing = false;
// if (error.status == '403') {
// this.eventBusService.emit(new EventData('logout', null));
// }
return throwError(() => error);
})
);
}
}
return next(req);
};
return next(req).pipe(
catchError((error) => {
if (
error instanceof HttpErrorResponse &&
!req.url.includes('auth/signin') &&
error.status === 401
) {
return handle401Error(isRefreshing, req, next);
}
return throwError(() => error);
})
);
};
修订方法 => 返回 Observable:authService.refreshToken() 应该返回一个在令牌刷新后发出的 observable。这使得handle401Error仅在刷新完成后才能完成。
=> 使用主题进行刷新过程:主题可以管理在令牌刷新时遇到 401 错误的多个调用。如果多个请求同时失败,这将有助于避免竞争条件或重复刷新尝试。
import { Subject, throwError } from 'rxjs';
import { switchMap, catchError, filter, take, tap } from 'rxjs/operators';
let isRefreshing = false;
const refreshSubject = new Subject<boolean>();
const handle401Error = (
req: HttpRequest<any>,
next: HttpHandlerFn
) => {
if (!isRefreshing) {
isRefreshing = true;
authService.refreshToken().pipe(
tap(() => {
isRefreshing = false;
refreshSubject.next(true); // Notify others waiting for the refresh
}),
catchError((err) => {
isRefreshing = false;
refreshSubject.next(false); // Notify failure
authService.logout();
router.navigate(['/login'], { queryParams: { returnUrl: req.url } });
return throwError(() => err);
})
).subscribe();
}
// Wait for the refresh to complete
return refreshSubject.pipe(
filter(isRefreshed => isRefreshed), // Proceed only if refresh succeeded
take(1), // Only take the first successful refresh event
switchMap(() => next(req))
);
};
// In the interceptor catchError block:
return next(req).pipe(
catchError((err: any) => {
if (err instanceof HttpErrorResponse && err.status === 401) {
return handle401Error(req, next);
}
return throwError(() => err);
})
);
说明
注释 此方法应确保刷新的令牌就位以供后续重试。 对于您提到的请求排队功能,此设置已经通过利用刷新主题奠定了基础,它可以处理多个排队请求。