如何在 angular http 拦截器中以异步方式缓存 http 请求?

How to cache http requests in async style in angular http inteceptor?

我正在编写 angular 5 应用程序。 authentication service

中有refreshAccessToken
refreshAccessToken(): Observable<ICredentials> {
     const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
     const httpParams = new HttpParams()
       .append('grant_type', 'refresh_token')
       .append('refresh_token', this.credentials.refresh_token)
       .append('client_id', Constants.CLIENT_ID)
       .toString();

     const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');

     return this.http.post(refreshTokenUrl, httpParams, { headers })
       .map((response: any) => {
         this.setCredentials(response);
         localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
         return response;
  });
}

我想实现下一个算法:

  1. 任何 http 请求因未授权而失败,状态为 401
  2. 尝试从服务器获取新的访问令牌
  3. 重复请求

在获取新的访问令牌时,可以创建新的 http 请求,在这种情况下,我想存储它们并在收到新的访问令牌后重复。为了达到这个目的,我编写了拦截器。

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '@app/core/authentication/authentication.service';
import { Urls, UrlsService } from '@app/shared/urls';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class UnauthorizedRequestInterceptor implements HttpInterceptor {
  newAccessToken$: Observable<ICredentials> = null;

  constructor(
    public authService: AuthenticationService,
    private router: Router,
    private urlsService: UrlsService) {
  }

  addAuthHeader(request: HttpRequest<any>) {
    if (this.authService.getCredentials()) {
      return request.clone({
        setHeaders: {
          'Authorization': 'Bearer ' + this.authService.getCredentials().access_token
        }
      });
    }
    return request;
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    request = this.addAuthHeader(request);

    return next.handle(request).catch((error: HttpErrorResponse) => {
      let handleRequests$ = null;

      if (this.isNeedNewAccessToken(error, request)) {
        handleRequests$ = this.handleRequestWithNewAccessToken(request, next);
      }

      return handleRequests$ ||
        (this.isUnathorizedError(error)
          ? Observable.empty()
          : Observable.throw(error));
    });
  }

  logout() {
    this.authService.logout();
    this.router.navigate(['login']);
  }

  private isNeedNewAccessToken(error: HttpErrorResponse, request: HttpRequest<any>): boolean {
    return this.isUnathorizedError(error)
      && this.authService.isAuthenticated()
      && this.isSignInRequest(request);
  }

  private getNewAccessToken(): Observable<ICredentials> {
    if (!this.newAccessToken$) {
      this.newAccessToken$ = this.authService.refreshAccessToken();
    }
    return this.newAccessToken$;
  }

  private isUnathorizedError(error: HttpErrorResponse) {
    return error.status === 401;
  }

  private handleRequestWithNewAccessToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    return this.getNewAccessToken()
      .mergeMap(() => {
        request = this.addAuthHeader(request);
        return next.handle(request);
      })
      .catch((err: HttpErrorResponse) => {
        if (err.error.error === 'invalid_grant') {
          this.logout();
        }
        return Observable.empty();
      });
  }

  private isNotSignInRequest(request: HttpRequest<any>): boolean {
    return request.url !== this.urlsService.getUrl(Urls.TOKEN);
  }
}

这个拦截器的行为真的很奇怪。在 handleRequestWithNewAccessToken 上的每个 mergeMap 上,angular 启动新的 post httpRequest。我预计从 refreshAccessToken(来自 authenticationService 的函数,顶部的代码)返回的可观察对象只会被解析一次。我不明白为什么每个合并地图都会触发它?我期待下一个:

  1. 我有可观察的 - 令牌的 http 请求
  2. 我使用 mergeMap - 当 http 请求完成时,将执行所有使用 mergeMap 添加的回调。

我想把我需要处理的请求存储在全局变量中,并在 subscribe() 到 http 请求中调用它们,但是有问题,每个请求都应该在初始流中解析拦截器。我不能这样做:.subscribe(token => this.httpClient.request(storedRequest) 因为这会创建新请求,所以所有操作都应该在可观察链内发生。

你能帮我找到解决办法吗?

PS 此解决方案有效,但我想摆脱不必要的令牌请求,f.e。如果页面需要发出 5 次请求并且令牌已过期 - 拦截器将发出 5 次令牌请求。

我认为你的代码很好,你所要做的就是share新令牌的请求。

refreshAccessToken(): Observable<ICredentials> {
        const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
        const httpParams = new HttpParams()
            .append('grant_type', 'refresh_token')
            .append('refresh_token', this.credentials.refresh_token)
            .append('client_id', Constants.CLIENT_ID)
            .toString();

        const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');

        return this.http.post(refreshTokenUrl, httpParams, { headers })
            .map((response: any) => {
                this.setCredentials(response);
                localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
                return response;
            })
            .share(); // <- HERE
    }

注意 share 运算符在 return

末尾

编辑:

我还认为您永远不会将 this.newAccessToken$ 退回到 null。也许考虑像这样将 null 添加到 finally

private getNewAccessToken(): Observable<ICredentials> {
    if (!this.newAccessToken$) {
        this.newAccessToken$ = this.authService.refreshAccessToken()
            .finally(() => {
                this.newAccessToken$ = null;
            });
    }
    return this.newAccessToken$;
}