【问题标题】:How to cache http requests in async style in angular http inteceptor?如何在角度http拦截器中以异步方式缓存http请求?
【发布时间】:2019-01-29 21:57:52
【问题描述】:

我正在编写 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 上,角度开始新的发布 httpRequest。我预计从refreshAccessToken(来自authenticationService 的函数,顶部的代码)返回的可观察对象只会被解析一次。我不明白为什么每个合并地图都会触发它?我期待下一个:

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

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

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

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

【问题讨论】:

    标签: javascript angular rxjs operators observable


    【解决方案1】:

    我认为您的代码很好,您只需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。也许可以考虑像这样将设置添加到nullfinally

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

    【讨论】:

    • 谢谢,share 成功了,我已将其添加到 getNewAccessToken:this.newAccessToken$ = this.authService.refreshAccessToken().share(); this.newAccessToken$.finally(() =&gt; this.newAccessToken$ = null) 中。另外为了启用finally,我添加了导入import 'rxjs/add/operator/finally';
    猜你喜欢
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多