【问题标题】:Angular HttpInterceptor request to retrieve new JWT token causes infinite loop检索新 JWT 令牌的 Angular HttpInterceptor 请求导致无限循环
【发布时间】:2020-02-01 12:28:07
【问题描述】:

在我的应用程序中,我使用 Angular HttpInterceptor 来跟踪每个传出的 HTTP 请求,并将存储在本地存储中的 JWT 令牌添加到请求标头中,以便后端 API 可以通过检查 JWT 令牌来授权请求​​。

为此,拦截器只需获取 HTTP 请求并通过设置标头添加令牌,然后调用 next.handle(req) 来传播请求。

这是使用以下代码完成的:

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

  /**
   * Add access_token to header.
   * @param req
   */
  addHeaders(req: HttpRequest<any>): HttpRequest<any> {
    return req.clone({
      setHeaders: {
        "Content-type": "application/json",
        token: this._authService.getAuthenticationToken(),
        jwt_token: this._authService.getAuthorizationToken(),
        [this._config.subscriptionKeyName]: this._config.subscriptionKey || ""
      }
    });
  }

我想扩展此功能,以便拦截器首先检查以确保存储在浏览器本地存储中的 JWT 令牌有效。如果令牌无效,则拦截器本身应触发单独的 HTTP 请求,以从 API 端点检索新的 JWT 令牌,该令牌应用于在继续原始请求之前覆盖旧令牌。

我试图弄清楚如何在我的 Angular 代码中构建这种行为,但我一直陷入无限循环。

这是我目前拥有的代码。

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    if (req.headers.has("newtokenrequest")) {
      next.handle(req);
    }

    let jwtToken: string = this._authService.getLocalAuthorizationToken();

    if (!this.checkJwtTokenIsValid(jwtToken)) { //Invalid token, get new one
      const authTokenStream = this._authService.getApiAuthorizationToken$();
      authTokenStream.subscribe(token => {
        this._authService.setAuthorizationToken(token);
      });

    }

    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

这是返回 Observable&lt;string&gt; 的代码,用于检索新的 JWT 令牌。

getApiAuthorizationToken$(): Observable<string> {
    let headers = new HttpHeaders();
    headers = headers.set('newtokenrequest', 'true');
    return this._httpClient.get<string>(this._configService.getConfig().teacherAuthAPI, {headers: headers});
  }

代码下到 authTokenStream 订阅,然后再次循环回到顶部。我认为这是因为订阅正在向后端发送一个新的 HTTP 请求以检索令牌,但是此请求应包含 newtokenrequest 作为标头,应由应调用 next.handle(req) 的拦截方法检测到,允许请求在没有进一步交互的情况下继续进行,但是这不会发生,而是代码循环。

编辑:更新

intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    if (req.headers.has("newtokenrequest")) {
      return next.handle(req);
    }

    let jwtToken: string = this._authService.getLocalAuthorizationToken();

    if (!this.checkJwtTokenIsValid(jwtToken) || jwtToken === undefined) { //Invalid token, get new one
      const authTokenStream = this._authService.getApiAuthorizationToken$();
      authTokenStream.subscribe(token => {
        this._authService.setAuthorizationToken(token);
        return next.handle(req);
      });
    }

    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

  getApiAuthorizationToken$(): Observable<string> {
    let headers = new HttpHeaders({
      newtokenrequest: 'true'
    });
    // return this._httpClient.get<string>(this._configService.getConfig().teacherAuthAPI, {headers: headers});
    const req = new HttpRequest<string>('GET', this._configService.getConfig().teacherAuthAPI, {headers});
    let res = this._httpBackend.handle(req).pipe(map((response: HttpResponse<string>) => response.body),);
    return res;
  }

【问题讨论】:

  • 为什么要在发送前检查令牌?客户端应用程序应该是最愚蠢的,你所有的逻辑都应该在后端。 API 有多种方法来验证令牌或刷新它。只需发送令牌,让它处理它?
  • 你在找itnext.io/…
  • 您的 Angular 代码不应检查 JWT 令牌的有效性。后端服务是否负责该任务,返回错误(可能是 401 Unauthorized)。然后,Angular 应用程序应该重定向到登录组件或调用将提供另一个令牌的授权服务。顺便说一句,你试过@auth0/angular-jwt 吗?很容易添加到您的应用程序中,您将不再需要拦截器。

标签: angular rxjs


【解决方案1】:

您应该做的是将检查本地存储有效性的工作分配给 authguard。

canActivate(): Observable<boolean> | Promise<boolean> | boolean {

        switch (this.ispectUrl(this.url)) {
    // I'm checking if there is a jwt in url
          case true: {
            const token = this.refactorUrl(this.url);
            this.auth.setAccessToken(token);
            return this.auth.validateJwt();
            break;
          }
    // No jwt in url? I check possibile jwt in localstorage
          case false: {
            return this.auth.validateJwt();
            break;
          }
    // Neither of both, simply i logout to login page session expired
          default: {
            this.auth.logout();
            return false;
          }

        }
    }

这里我从 url 中提取 jwt,因为我使用 CAS 实现,但如果 url 上没有任何 jwt,我只需在执行 anithing 之前检查本地存储,如果 jwt 有效,用户可以导航所有受 authguard 保护的 url,请记住,在检查 jwt 有效性后,您应该提取 jwt 到期日期,并从中知道何时检查刷新。

【讨论】:

    【解决方案2】:

    您可以通过使用HttpBackend 服务而不是HttpClient 来避免在请求授权令牌时出现循环。 HttpBackendHttpClient 使用的低级 Http 服务。它不考虑拦截器。

    import {HttpBackend, HttpHeaders, HttpRequest, HttpResponse} from '@angular/common/http';
    
    constructor(private httpBackend: HttpBackend) {
    }
    
    getApiAuthorizationToken$(): Observable<string> {
      const headers = new HttpHeaders({
        newtokenrequest: 'true'
      });
    
      const request = new HttpRequest<string>(
        'GET',
        this._configService.getConfig().teacherAuthAPI,
        {
          headers
        });
    
      return this.httpBackend.handle(request).pipe(
        map((response: HttpResponse<string>) => response.body),
      );
    }
    

    更新。

    替换

    const authTokenStream = this._authService.getApiAuthorizationToken$();
    authTokenStream.subscribe(token => {
      this._authService.setAuthorizationToken(token);
      return next.handle(req);
    });
    

    return this.this._authService.getApiAuthorizationToken$().pipe(
      tap(token => this._authService.setAuthorizationToken(token)),
      switchMap(() => {
        req = this.addHeaders(req);
        return next.handle(req);
      })
    );
    

    尽量避免在拦截器内部调用订阅。

    【讨论】:

    • 我已经尝试实现你的想法,但是现在当我订阅请求时,我没有按预期得到我的 JWT,而是得到一个未定义的。我已经用我的新代码更新了这个问题,你能看出我做错了什么吗?
    • 更新了响应。您的HttpBackend 电话看起来有效。尝试调试它以找出它返回 undefined 的原因。
    • this._httpBackend.handle(req).pipe(tap(console.log), map((response: HttpResponse&lt;string&gt;) =&gt; response.body),); 在 console.log 中添加 tap 将显示响应对象。这可能会有所帮助。
    猜你喜欢
    • 2018-11-07
    • 1970-01-01
    • 2022-06-14
    • 2019-06-26
    • 1970-01-01
    • 2020-08-25
    • 2019-11-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多