如何防止多个 Refreshtoken 调用并在 accesstoken 过期时提供所有挂起的 api(已返回 401)

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

我已经通过引用此链接Reference Link使用拦截器实现了refreshtoken逻辑。 它按预期工作正常。但在这种情况下,我面临一个关键问题,即当我并行调用多个 api 并且所有 api 返回 401 时,refreshtoken 调用不止一次,具体取决于 this.isRefreshing 变量值设置为 true 的速度。现在我的问题是,我只想调用 refreshtoken api 一次,必须调用所有其他待处理的 api(已返回 401)。如何实现?

有3个api调用。全部返回 401,因此所有人都将尝试调用 refreshtoken。只应调用一个 refreshtoken api,所有其他红色 api 应一一或并行提供。

以下是我的interceptor.ts.

    import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable, Injector } from "@angular/core";
import { BehaviorSubject, Observable, throwError } from "rxjs";
import { catchError, filter, switchMap, take } from "rxjs/operators";
import { AuthenticationService, CommonService } from "../services";
import { TokenService } from "../services/token.service";

/**
 * Link Refere : https://stackoverflow.com/questions/57637923/angular-8-intercept-call-to-refresh-token
 */
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    private tokenService;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
    /* No Direct Service inject on Constructor
       Using Like: 
       const authService = this.injector.get(AuthenticationService);
    */
    constructor(private injector: Injector) {
        this.tokenService = this.injector.get(TokenService);
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        //const token = localStorage.getItem('accessToken');

        // const token = this.tokenService.accessToken;

        // if (token) {
        //     req = this.addTokenHeader(req, token);
        // }

        req = req.clone({
            withCredentials: true
        });

        return next.handle(req).pipe(catchError(error => {
            // if (error instanceof HttpErrorResponse && !req.url.includes('auth/signin') && error.status === 401) {
            //     return this.handle401Error(req, next);
            // }

            if (error instanceof HttpErrorResponse && error.status === 401) {
                return this.handle401Error(req, next);
            }

            return throwError(error);
        }));
    }

    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        console.log("401 error request:", request);
        const authService = this.injector.get(AuthenticationService);
        if (!this.isRefreshing) {
            this.isRefreshing = true;
            this.refreshTokenSubject.next(null);

            return authService.refreshToken().pipe(
                switchMap((token: any) => {
                    this.isRefreshing = false;
                    this.tokenService.accessToken = token.accessToken;
                    this.refreshTokenSubject.next(token.accessToken);
                    return next.handle(request);
                    //return next.handle(this.addTokenHeader(request, token.accessToken));
                }),
                catchError((err) => {
                    this.isRefreshing = false;
                    authService.directRedirectToLogin();

                    // authService.logout().subscribe(data => {
                    //     authService.redirectToLogin();
                    // });
                    return throwError(err);
                })
            );
        }

        return this.refreshTokenSubject.pipe(
            filter(token => token !== null),
            take(1),
            switchMap((token) => next.handle(request))
            //switchMap((token) => next.handle(this.addTokenHeader(request, token)))
        );
    }

    private addTokenHeader(request: HttpRequest<any>, token: string) {
        const setHeaders = {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json'
        }

        if (request.body instanceof FormData) {
            delete setHeaders["Content-Type"];
        }

        return request.clone({
            setHeaders: setHeaders
        });
    }
}
angular authentication access-token interceptor refresh-token
2个回答
0
投票

更新 02.12.2022: 这是一个很好的例子! GitHub

它使用的服务将等待刷新令牌(或刷新时出错)!试试这个!


这是我在这种情况下的拦截器:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse,
} from '@angular/common/http';
import {
  BehaviorSubject,
  catchError,
  filter,
  Observable,
  switchMap,
  take,
  throwError,
} from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class BearerInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
    null
  );

  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<Object>> {
    let authReq = req;
    const token = this.authService.getToken();
    if (token != null) {
      authReq = this.addTokenHeader(req, token);
    }

    return next.handle(authReq).pipe(
      catchError((error) => {
        if (
          error instanceof HttpErrorResponse &&
          !authReq.url.includes('auth/signin') &&
          error.status === 401
        ) {
          if (authReq.url.includes('auth/token'))
            return throwError(() => new Error(error.error));

          return this.handle401Error(authReq, next);
        }

        return throwError(() => new Error(error));
      })
    );
  }

  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      const token = this.authService.getRefreshToken();

      if (token)
        return this.authService.refreshToken(token).pipe(
          switchMap((token: any) => {
            this.isRefreshing = false;
            this.authService.saveTokens(token.token, token.refreshToken);
            this.refreshTokenSubject.next(token.accessToken);

            return next.handle(this.addTokenHeader(request, token.token));
          }),
          catchError((err) => {
            this.isRefreshing = false;

            this.authService.signOut();
            return throwError(() => new Error(err));
          })
        );
    }

    return this.refreshTokenSubject.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap((token) => next.handle(this.addTokenHeader(request, token)))
    );
  }

  private addTokenHeader(request: HttpRequest<any>, token: string) {
    /* for Spring Boot back-end */
    // return request.clone({ headers: request.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });

    /* for Node.js Express back-end */
    return request.clone({
      headers: request.headers.set('Authorization', 'Bearer ' + token),
    });
  }
}

我认为重要的部分是检查错误来自哪里:

if (error instanceof HttpErrorResponse &&
    !authReq.url.includes('auth/signin') &&
    error.status === 401) {
        if (authReq.url.includes('auth/token'))
            return throwError(() => new Error(error.error));

          return this.handle401Error(authReq, next);
        }

所以包括 url auth/token 错误来自令牌。否则全部保留并等待刷新令牌。

玩得开心,弗洛里安


0
投票

试试这个,我也遇到了同样的问题,但现在使用它工作正常。

import { Injectable } from '@angular/core';
    import {
      HttpEvent,
      HttpHandler,
      HttpInterceptor,
      HttpRequest
    } from '@angular/common/http';
    import { throwError, Observable, BehaviorSubject } from 'rxjs';
    import { catchError, concatMap, filter, finalize, take } from 'rxjs/operators';
    
    import { environment } from '@app/env';
    import { AuthService, RefreshTokenResult } from '../auth.service';
    
    @Injectable()
    export class AuthIntercepter implements HttpInterceptor {
      isRefreshingToken = false;
    
      tokenRefreshed$ = new BehaviorSubject<boolean>(false);
    
      constructor(private authService: AuthService) {}
    
      addToken(req: HttpRequest<any>): HttpRequest<any> {
        const token = this.authService.token;
        return token
          ? req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })
          : req;
      }
    
      intercept(
        req: HttpRequest<any>,
        next: HttpHandler
      ): Observable<HttpEvent<any>> {
        return next.handle(this.addToken(req)).pipe(
          catchError((err) => {
            if (err.status === 401) {
              return this.handle401Error(req, next);
            }
    
            return throwError(err);
          })
        );
      }
    
      private handle401Error(
        req: HttpRequest<any>,
        next: HttpHandler
      ): Observable<any> {
        if (this.isRefreshingToken) {
          return this.tokenRefreshed$.pipe(
            filter(Boolean),
            take(1),
            concatMap(() => next.handle(this.addToken(req)))
          );
        }
    
        this.isRefreshingToken = true;
    
        // Reset here so that the following requests wait until the token
        // comes back from the refreshToken call.
        this.tokenRefreshed$.next(false);
    
        return this.authService.refreshToken().pipe(
          concatMap((res: RefreshTokenResult) => {
            if (!environment.production) {
              console.info('Token was successfully refreshed'); // tslint:disable-line
            }
    
            this.tokenRefreshed$.next(true);
            return next.handle(this.addToken(req));
          }),
          catchError((err) => {
            this.authService.logout();
            return throwError(err);
          }),
          finalize(() => {
            this.isRefreshingToken = false;
          })
        );
      }
    }
© www.soinside.com 2019 - 2024. All rights reserved.