Angular:遇到 401 错误时如何对 HTTP 请求进行排队?

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

我有一个使用 JWT 令牌进行身份验证的 Angular 应用程序。当 HTTP 请求返回 401 错误(未经授权)时,我需要刷新令牌并重试请求。

我之前的问题Angular HTTP拦截器等待http请求直到获得刷新令牌

我已经实现了一个 HTTP 拦截器,它通过调用刷新令牌并重试请求的函数来处理 401 错误。

当一次只有一个 HTTP 请求时,这种方法效果很好。但是,我的应用程序有多个需要并行执行的 HTTP 请求。我在路由解析器中使用 forkJoin 一次性执行所有请求,但是当其中一个请求返回 401 错误时,其他请求将继续执行并返回 401。

我想实现一个解决方案,将因 401 错误而失败的请求排队,直到刷新令牌,然后使用新令牌自动恢复它们。我怎样才能做到这一点?

以前我有

预先感谢您的帮助。

拦截器:

export class HttpErrorInterceptor implements HttpInterceptor {
    constructor(
        private _router: Router,
        private _logger: LoggerService,
        private _authService: AuthenticationService
    ) {}

    private isRefreshingToken = false;
    private tokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<
        string | null
    >(null);

    public intercept(
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<IResponse>> {
        return next.handle(request).pipe(
            timeout(appSettings.ajaxTimeout),
            catchError((error) => this.errorHandler(error, request, next))
        );
    }
    
    private errorHandler(
        error: HttpErrorResponse,
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<IResponse>> {
        if (error.error instanceof ErrorEvent) {
            if (!environment.production) {
                /**
                 * !A client-side or network error occurred. Handle it accordingly.
                 * !in development mode printing errors in console
                 */
                this._logger.log('Request error ' + error);
            }
        } else {
            const httpErrorCode: number = error['status'];
            switch (httpErrorCode) {
                case StatusCodes.INTERNAL_SERVER_ERROR:
                    this._router.navigate(['/internal-server-error']);
                    break;
                case StatusCodes.UNAUTHORIZED:
                    return this.handle401Error(request, next);
                default:
                    this._logger.log('Request error ' + error);
                    break;
            }
        }

        return throwError(() => error.error || error);
    }

    
    private handle401Error(
       request: HttpRequest<any>,
       next: HttpHandler
    ): Observable<HttpEvent<any>> {
        if (!this.isRefreshingToken) {
            this.isRefreshingToken = true;
            // Reset here so that the following requests wait until the token
            // comes back from the refreshToken call.
            this.tokenSubject.next(null);
            return this._authService.regenerateTokens().pipe(
                switchMap((apiResult) => {
                    const authData = apiResult.dataset as IAuthResult;
                    this._authService.updateRefreshedTokens(authData);

                    this.tokenSubject.next(authData.tokens.access_token);
                    return next.handle(
                        this.addTokenInHeader(
                            request,
                            authData.tokens.access_token
                        )
                    );
                }),
                catchError((error) => {
                    // If there is an exception calling 'refreshToken', bad news so logout.
                    this._authService.logout();
                    this._router.navigate(['/']);
                    return throwError(() => error);
                }),
                finalize(() => {
                    this.isRefreshingToken = false;
                })
            );
        } else {
            return this.tokenSubject.pipe(
                filter((token) => token !== null),
                take(1),
                switchMap((token) => {
                    return next.handle(this.addTokenInHeader(request, token));
                })
            );
        }
    }

    
    private addTokenInHeader(
        request: HttpRequest<any>,
        token: string | null
    ): HttpRequest<any> {
        return request.clone({
            setHeaders: { Authorization: 'Bearer ' + token }
        });
    }
}
angular rxjs angular-http-interceptors
1个回答
0
投票

听起来您想在遇到

401
响应时停止请求,并在令牌刷新后继续请求。

实现此目的的一种方法是从当前令牌值开始声明拦截器,使用该令牌处理请求,然后捕获 401 响应并启动刷新。这假设您有一个将当前令牌值公开为可观察值的服务,并且还有一个启动刷新的方法。

export const authInterceptor: HttpInterceptorFn = (request, next) => {
  const authService = inject(AuthService);

  return authService.token$.pipe(
    filter(token => !!token),
    map(token => request.clone({setHeaders: { Authorization: `Bearer ${token}`}})),
    switchMap(authRequest => next(authRequest).pipe(
      catchError(error => {
        if(error.status === 401) {
          authService.refreshToken();
          return EMPTY; 
        }
        return throwError(() => error);
      })
    )),
    takeWhile(event => !(event instanceof HttpResponse), true)
  );
};

通过上面的代码,您可以看到,我们不是直接调用

next(request)
,而是创建一个以
token$
observable 开头的 observable,过滤掉
undefined
令牌,将有效令牌应用于标头并继续请求。

然后我们捕获响应中的潜在错误,启动令牌刷新。由于

refreshToken()
上的
authService
方法将导致
token$
发出
undefined
,因此任何其他请求将有效地暂停,直到发出新令牌。

最后,我们使用

takeWhile
只允许一个
HttpResponse
响应(我们不想在收到 HttpResponse 后令牌发生变化时重复请求)。

管道中各个操作员总结:

  • filter
    - 暂停执行,直到收到非空令牌
  • map
    - 将 auth 标头附加到请求中
  • switchMap
    - 处理请求
  • catchError
    - 识别
    401
    响应并启动令牌刷新;返回空可观察值
  • throwError
    - 转发未处理的错误响应
  • takeWhile
    - 限制为 1 个
    HttpResponse
    ;使用
    inclusive: true
    发出最终响应。

这是一个 StackBlitz 示例。

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