多个 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
。
我在 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
。