多个 401 Angular 令牌拦截器

Multiple 401 Angular Token Interceptor

我在 Angular 中创建了一个令牌拦截器,我用它来刷新我的 JWT 令牌。 不幸的是,我不知道为什么,几次调用都失败了(错误 401),当拦截器检索到新令牌时,只有最后一次失败的重做。 这意味着它错过了一些电话并且我的 UI 没有正确填充。

我尽量给你留几段代码和一张照片。

这是我的令牌拦截器:

import { HTTP_INTERCEPTORS, HttpErrorResponse, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Router } from '@angular/router';

import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';

import { MatDialog } from '@angular/material/dialog';

import { TranslateService } from '@ngx-translate/core';

import { G3SnackType } from '@cgm/g3-component-lib';
import { G3SnackbarService } from '@cgm/g3-component-lib';

import { AuthFacade } from '@g3p/auth-shell/store/auth.facade';
import { AuthService } from '@g3p/auth-shell/services/auth.service';
import { ITokenError, ITokenErrorModal } from '@g3p/auth-shell/interfaces/token-error.interface';
import { LoggerService } from '@g3p/shared/error-handler/logger.service';
import { SessionStorageJwtService } from '@g3p/auth-shell/services/session-storage-jwt.service';
import { Token } from '@g3p/auth-shell/interfaces/token.interface';
import { TokenErrorType } from '@g3p/auth-shell/interfaces/token-error.enum';
import { TokenErrorComponent } from '@g3p/auth-shell/components/token-error/token-error.component';

const tokenErrorModalSettings = {
  width: '25.75rem',
  height: '15.5rem',
  disableClose: true,
  autoFocus: false,
  data: {} as ITokenErrorModal
};

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

  constructor(
    private authFacade: AuthFacade,
    private authService: AuthService,
    private dialog: MatDialog,
    private loggerService: LoggerService,
    private router: Router,
    private sessionStorageJwtService: SessionStorageJwtService,
    private snackbarService: G3SnackbarService,
    private translateService: TranslateService,
  ) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    let token: Token = {} as Token;
    this.sessionStorageJwtService.getToken().subscribe(t => (token = t));

    let refreshToken = '';
    this.sessionStorageJwtService.getRefreshToken().subscribe(t => (refreshToken = t));

    const path = request.url.split('/');
    if (
      path.includes('assets') ||
      path.includes('workstationregistration') ||
      (this.router.url.includes('rolespermission') && path.includes('users')) ||
      (this.router.url.includes('/auth') && path.includes('token'))
    ) {
      return next.handle(request);
    }

    return next.handle(request).pipe(catchError(error => {
      if (error instanceof HttpErrorResponse && error.status === 401) {
        if ((error.error as ITokenError).error_description.includes(TokenErrorType.ACCESS_TOKEN_EXPIRED)) {
          return this.handle401Error(request, next, refreshToken);
        }
        if ((error.error as ITokenError).error_description.includes(TokenErrorType.INVALID_REFRESH_TOKEN)) {
          this.showSnackbarTokenError(this.translateService.instant('content.invalid-refresh-token'));
        }
        if ((error.error as ITokenError).error_description.includes(TokenErrorType.CANNOT_CONVERT_ACCESS_TOKEN)) {
          this.showSnackbarTokenError(this.translateService.instant('content.cannot-convert-access-token'));
        }
        if ((error.error as ITokenError).error_description === TokenErrorType.USER_LOGGED_INTO_ANOTHER_WORKSTATION) {
          this.showModalTokenError(this.translateService.instant('content.user-logged-into-another-workstation'));
        }
        if ((error.error as ITokenError).error_description === TokenErrorType.USER_DELETED) {
          this.showModalTokenError(this.translateService.instant('content.user-deleted'));
        }
        if ((error.error as ITokenError).error_description === TokenErrorType.USER_CLAIMS_CHANGED) {
          this.showModalTokenError(this.translateService.instant('content.user-claims-changed'));
        }
        if ((error.error as ITokenError).error_description.includes(TokenErrorType.TECHNICAL_LOGOUT)) {
          this.showModalTokenError(this.translateService.instant('content.technical-logout'));
        }
      }
      return throwError(error);
    }));
  }

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

      if (rToken && rToken !== '') {
        const body = new URLSearchParams();
        body.set('grant_type', 'refresh_token');
        body.set('refresh_token', rToken);

        return this.authService.fetchToken$(body).pipe(
          switchMap((newToken: Token) => {
            this.isRefreshing = false;

            this.sessionStorageJwtService.setToken(newToken);
            this.refreshTokenSubject.next(newToken);

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

            this.authFacade.logout();
            return throwError(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: Token) {
    return request.clone({ headers: request.headers.set('Authorization', `Bearer ${token.access_token}`) });
  }

  private showSnackbarTokenError(message: string) {
    this.snackbarService.open(message, 5000, G3SnackType.Error);
    return this.authFacade.logout();
  }

  private showModalTokenError(message: string) {
    tokenErrorModalSettings.data.errorMessage = message;

    if (this.dialog.openDialogs.length < 1) {
      const dialogRef = this.dialog.open(TokenErrorComponent, tokenErrorModalSettings);

      dialogRef.afterClosed().subscribe(data => {
        if (data?.errorMessage !== '') {
          return this.authFacade.logout();
        }
      });
    }
  }
}

export const authInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
];

您可以尝试将拦截器更改为如下内容:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
  // 1 >>>> This should return the token as string to be injected to the requests directly
  const token: string = this.sessionStorageJwtService.getToken();

  // 2 >>>> Move this part to the handle401Error method
  // let refreshToken = "";
  // this.sessionStorageJwtService.getRefreshToken().subscribe((t) => (refreshToken = t));

  const path = request.url.split('/');
  if (
    path.includes('assets') ||
    path.includes('workstationregistration') ||
    (this.router.url.includes('rolespermission') && path.includes('users')) ||
    (this.router.url.includes('/auth') && path.includes('token'))
  ) {
    return next.handle(request);
  }

  // 3 >>>> Inject the token into the request header
  request = this.addTokenHeader(request, token);

  return next.handle(request).pipe(
    catchError((error) => {
      if (error instanceof HttpErrorResponse && error.status === 401) {
        if (
          (error.error as ITokenError).error_description.includes(
            TokenErrorType.ACCESS_TOKEN_EXPIRED
          )
        ) {
          return this.handle401Error(request, next);
        }
        // Handle the other errors here...
      }
      return throwError(error);
    })
  );
}

private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
  if (!this.isRefreshing) {
    this.isRefreshing = true;
    // Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
    this.refreshTokenSubject.next(null);

    // 4 >>>> Chain the getRefreshToken with the fetchToken$ observables like the following:
    return this.sessionStorageJwtService.getRefreshToken().pipe(
      switchMap((rToken) => {
        if (!!rToken) {
          const body = new URLSearchParams();
          body.set('grant_type', 'refresh_token');
          body.set('refresh_token', rToken);
          return this.authService.fetchToken$(body);
        }

        throwError('Refresh token invalid');
      }),
      switchMap((newToken: Token) => {
        // When the call to refreshToken completes we reset the isRefreshing to false
        // for the next time the token needs to be refreshed
        if (newToken) {
          this.sessionStorageJwtService.setToken(newToken);
          this.refreshTokenSubject.next(newToken);

          return next.handle(this.addTokenHeader(request, newToken));
        }

        throwError('Refresh token invalid');
      }),
      catchError((err) => {
        this.authFacade.logout();
        return throwError(err);
      }),
      finalize(() => {
        this.isRefreshing = false;
      })
    );
  } else {
    // 5 >>>> Move this return to this block instead of above one, to be returned if the toke refresh is still on-progress
    return this.refreshTokenSubject.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap((token) => next.handle(this.addTokenHeader(request, token)))
    );
  }
}

您尝试使用的拦截器存在一些逻辑错误,例如:

  • 它不会将获取的令牌注入到请求中,应该注入令牌以正确验证请求。最好return将当前令牌作为字符串直接注入到请求中。
  • 它调用 getRefreshToken 来获取刷新令牌,但您没有等待它完成。但它应该移动到 handle401Error 方法并与 this.authService.fetchToken$(body) observable 链接。
  • 它 return 来自错误块的 refreshTokenSubject