使用来自 HttpIntercopterFn 中捕获错误的新鲜 cookie

问题描述 投票:0回答:2

我创建了一个拦截器来确保每个请求都发送

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()都没有被执行。

angular rxjs jwt interceptor angular-http-interceptors
2个回答
0
投票

您代码中的问题是订阅中的代码是

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);
    })
  );
};

0
投票

修订方法 => 返回 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);
    })
  );

说明

  • isRefreshing 标志:确保每次只进行一次刷新尝试 时间。
  • refreshSubject:刷新时将所有传入请求排队 正在进行中。它仅允许在令牌存在时继续请求 刷新(发出 true)或发生错误(发出 false)。
  • 过滤并获取:确保等待刷新的请求仅在完成并成功后才继续。

注释 此方法应确保刷新的令牌就位以供后续重试。 对于您提到的请求排队功能,此设置已经通过利用刷新主题奠定了基础,它可以处理多个排队请求。

© www.soinside.com 2019 - 2024. All rights reserved.