令牌刷新后的角度4拦截器重试请求

嗨,我试图找出如何实现新的角度拦截器和处理 401 unauthorized错误刷新令牌和重试的请求。这是我一直遵循的指南: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

我成功地缓存了失败的请求,并且可以刷新令牌,但是我不知道如何重新发送以前失败的请求。我还想让它与我目前使用的解析器一起工作。

标记,拦截器

return next.handle( request ).do(( event: HttpEvent<any> ) => {
if ( event instanceof HttpResponse ) {
// do stuff with response if you want
}
}, ( err: any ) => {
if ( err instanceof HttpErrorResponse ) {
if ( err.status === 401 ) {
console.log( err );
this.auth.collectFailedRequest( request );
this.auth.refreshToken().subscribe( resp => {
if ( !resp ) {
console.log( "Invalid" );
} else {
this.auth.retryFailedRequests();
}
} );


}
}
} );

身份验证服务

cachedRequests: Array<HttpRequest<any>> = [];


public collectFailedRequest ( request ): void {
this.cachedRequests.push( request );
}


public retryFailedRequests (): void {
// retry the requests. this method can
// be called after the token is refreshed
this.cachedRequests.forEach( request => {
request = request.clone( {
setHeaders: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${ this.getToken() }`
}
} );
//??What to do here
} );
}

上面的 retryFailedRequest ()文件是我无法解决的问题。如何重新发送请求,并在重试后通过解析器将其提供给路由?

如果有帮助的话,这是所有相关代码: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9

75304 次浏览

我也遇到了类似的问题,我认为收集/重试逻辑过于复杂。相反,我们可以使用 catch 操作符检查401,然后查看令牌刷新,并重新运行请求:

return next.handle(this.applyCredentials(req))
.catch((error, caught) => {
if (!this.isAuthError(error)) {
throw error;
}
return this.auth.refreshToken().first().flatMap((resp) => {
if (!resp) {
throw error;
}
return next.handle(this.applyCredentials(req));
});
}) as any;

...

private isAuthError(error: any): boolean {
return error instanceof HttpErrorResponse && error.status === 401;
}

在 entication.service.ts 中,应该注入一个 HttpClient 作为依赖项

constructor(private http: HttpClient) { }

然后,您可以按照以下步骤重新提交请求(在 retryFailedRequest 内部) :

this.http.request(request).subscribe((response) => {
// You need to subscribe to observer in order to "retry" your request
});

我的最终解决方案。工程与并行请求。

更新: 使用 Angular 9/RxJS 6更新的代码,错误处理和修复了当刷新令牌失败时的循环

import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http";
import { Injector } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, Observable, throwError } from "rxjs";
import { catchError, switchMap, tap} from "rxjs/operators";
import { AuthService } from "./auth.service";


export class AuthInterceptor implements HttpInterceptor {


authService;
refreshTokenInProgress = false;


tokenRefreshedSource = new Subject();
tokenRefreshed$ = this.tokenRefreshedSource.asObservable();


constructor(private injector: Injector, private router: Router) {}


addAuthHeader(request) {
const authHeader = this.authService.getAuthorizationHeader();
if (authHeader) {
return request.clone({
setHeaders: {
"Authorization": authHeader
}
});
}
return request;
}


refreshToken(): Observable<any> {
if (this.refreshTokenInProgress) {
return new Observable(observer => {
this.tokenRefreshed$.subscribe(() => {
observer.next();
observer.complete();
});
});
} else {
this.refreshTokenInProgress = true;


return this.authService.refreshToken().pipe(
tap(() => {
this.refreshTokenInProgress = false;
this.tokenRefreshedSource.next();
}),
catchError(() => {
this.refreshTokenInProgress = false;
this.logout();
}));
}
}


logout() {
this.authService.logout();
this.router.navigate(["login"]);
}


handleResponseError(error, request?, next?) {
// Business error
if (error.status === 400) {
// Show message
}


// Invalid token error
else if (error.status === 401) {
return this.refreshToken().pipe(
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
catchError(e => {
if (e.status !== 401) {
return this.handleResponseError(e);
} else {
this.logout();
}
}));
}


// Access denied error
else if (error.status === 403) {
// Show message
// Logout
this.logout();
}


// Server error
else if (error.status === 500) {
// Show message
}


// Maintenance error
else if (error.status === 503) {
// Show message
// Redirect to the maintenance page
}


return throwError(error);
}


intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
this.authService = this.injector.get(AuthService);


// Handle request
request = this.addAuthHeader(request);


// Handle response
return next.handle(request).pipe(catchError(error => {
return this.handleResponseError(error, request, next);
}));
}
}


export const AuthInterceptorProvider = {
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
};

基于 这个例子,这是我的作品

@Injectable({
providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {


constructor(private loginService: LoginService) { }


/**
* Intercept request to authorize request with oauth service.
* @param req original request
* @param next next
*/
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
const self = this;


if (self.checkUrl(req)) {
// Authorization handler observable
const authHandle = defer(() => {
// Add authorization to request
const authorizedReq = req.clone({
headers: req.headers.set('Authorization', self.loginService.getAccessToken()
});
// Execute
return next.handle(authorizedReq);
});


return authHandle.pipe(
catchError((requestError, retryRequest) => {
if (requestError instanceof HttpErrorResponse && requestError.status === 401) {
if (self.loginService.isRememberMe()) {
// Authrozation failed, retry if user have `refresh_token` (remember me).
return from(self.loginService.refreshToken()).pipe(
catchError((refreshTokenError) => {
// Refresh token failed, logout
self.loginService.invalidateSession();
// Emit UserSessionExpiredError
return throwError(new UserSessionExpiredError('refresh_token failed'));
}),
mergeMap(() => retryRequest)
);
} else {
// Access token failed, logout
self.loginService.invalidateSession();
// Emit UserSessionExpiredError
return throwError(new UserSessionExpiredError('refresh_token failed'));
}
} else {
// Re-throw response error
return throwError(requestError);
}
})
);
} else {
return next.handle(req);
}
}


/**
* Check if request is required authentication.
* @param req request
*/
private checkUrl(req: HttpRequest<any>) {
// Your logic to check if the request need authorization.
return true;
}
}

您可能想检查用户启用 Remember Me是否使用刷新令牌重试或只是重定向到注销页面。

仅供参考,LoginService有以下方法:
- getAccessToken () : string-返回当前的 access_token
- isRememberMe () : boolean-检查用户是否有 refresh_token
刷新令牌() : 可观察/承诺-请求使用 refresh_token为新的 access_token授权服务器
- valididateSession () : void-删除所有用户信息并重定向到注销页面

我根据失败请求的 URL 创建了一个新请求,并发送了失败请求的相同主体。

 retryFailedRequests() {


this.auth.cachedRequests.forEach(request => {


// get failed request body
var payload = (request as any).payload;


if (request.method == "POST") {
this.service.post(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});


}
else if (request.method == "PUT") {


this.service.put(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});
}


else if (request.method == "DELETE")


this.service.delete(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});
});


this.auth.clearFailedRequests();

}

Ideally, you want to check isTokenExpired before request sent. And if expired refresh the token and add refreshed in the header.

除此之外,retry operator可能有助于解决在401响应上刷新令牌的逻辑。

在您发出请求的服务中使用 RxJS retry operator。它接受 retryCount参数。 如果没有提供,它将无限期地重试序列。

在响应拦截器中刷新令牌并返回错误。当您的服务返回错误,但是现在正在使用重试操作符,因此它将重试请求,这次使用刷新的令牌(Interceptor 使用刷新的令牌来添加头部)

import {HttpClient} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';


@Injectable()
export class YourService {


constructor(private http: HttpClient) {}


search(params: any) {
let tryCount = 0;
return this.http.post('https://abcdYourApiUrl.com/search', params)
.retry(2);
}
}

Andrei Ostrovski 的最终解决方案工作得非常好,但是如果刷新令牌也过期了(假设您正在进行一个用于刷新的 api 调用) ,那么就无法工作。经过深入研究,我意识到这个刷新令牌 API 调用也被拦截器拦截了。我必须添加一个 if 语句来处理这个问题。

 intercept( request: HttpRequest<any>, next: HttpHandler ):Observable<any> {
this.authService = this.injector.get( AuthenticationService );
request = this.addAuthHeader(request);


return next.handle( request ).catch( error => {
if ( error.status === 401 ) {


// The refreshToken api failure is also caught so we need to handle it here
if (error.url === environment.api_url + '/refresh') {
this.refreshTokenHasFailed = true;
this.authService.logout();
return Observable.throw( error );
}


return this.refreshAccessToken()
.switchMap( () => {
request = this.addAuthHeader( request );
return next.handle( request );
})
.catch((err) => {
this.refreshTokenHasFailed = true;
this.authService.logout();
return Observable.throw( err );
});
}


return Observable.throw( error );
});
}

使用最新版本的 Angular (7.0.0)和 rxjs (6.3.3) ,这就是我如何创建一个全功能的 Auto Session 恢复拦截器的方法,它可以确保,如果并发请求在401中失败,那么它也应该只需要点击一次令牌刷新 API,然后使用 switchMap 和 Subject 将失败的请求通过管道传递给该请求的响应。下面是拦截器代码的样子。我省略了我的认证服务和存储服务的代码,因为它们是相当标准的服务类。

import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, Subject, throwError } from "rxjs";
import { catchError, switchMap } from "rxjs/operators";


import { AuthService } from "../auth/auth.service";
import { STATUS_CODE } from "../error-code";
import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";


@Injectable()
export class SessionRecoveryInterceptor implements HttpInterceptor {
constructor(
private readonly store: StoreService,
private readonly sessionService: AuthService
) {}


private _refreshSubject: Subject<any> = new Subject<any>();


private _ifTokenExpired() {
this._refreshSubject.subscribe({
complete: () => {
this._refreshSubject = new Subject<any>();
}
});
if (this._refreshSubject.observers.length === 1) {
this.sessionService.refreshToken().subscribe(this._refreshSubject);
}
return this._refreshSubject;
}


private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
return (
error.status &&
error.status === STATUS_CODE.UNAUTHORIZED &&
error.error.message === "TokenExpired"
);
}


intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
return next.handle(req);
} else {
return next.handle(req).pipe(
catchError((error, caught) => {
if (error instanceof HttpErrorResponse) {
if (this._checkTokenExpiryErr(error)) {
return this._ifTokenExpired().pipe(
switchMap(() => {
return next.handle(this.updateHeader(req));
})
);
} else {
return throwError(error);
}
}
return caught;
})
);
}
}


updateHeader(req) {
const authToken = this.store.getAccessToken();
req = req.clone({
headers: req.headers.set("Authorization", `Bearer ${authToken}`)
});
return req;
}
}

根据@anton-toshik 的评论,我认为在一篇文章中解释这段代码的功能是个好主意。您可以阅读我的文章 给你,了解对这段代码的解释和理解(它是如何工作的以及为什么工作的?).希望能有帮助。

To support ES6 syntax the solution needs to be bit modify and that is as following also included te loader handler on multiple request




private refreshTokenInProgress = false;
private activeRequests = 0;
private tokenRefreshedSource = new Subject();
private tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
private subscribedObservable$: Subscription = new Subscription();






intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.activeRequests === 0) {
this.loaderService.loadLoader.next(true);
}
this.activeRequests++;


// Handle request
request = this.addAuthHeader(request);


// NOTE: if the flag is true it will execute retry auth token mechanism ie. by using refresh token it will fetch new auth token and will retry failed api with new token
if (environment.retryAuthTokenMechanism) {
// Handle response
return next.handle(request).pipe(
catchError(error => {
if (this.authenticationService.refreshShouldHappen(error)) {
return this.refreshToken().pipe(
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
catchError(() => {
this.authenticationService.setInterruptedUrl(this.router.url);
this.logout();
return EMPTY;
})
);
}


return EMPTY;
}),
finalize(() => {
this.hideLoader();
})
);
} else {
return next.handle(request).pipe(
catchError(() => {
this.logout();
return EMPTY;
}),
finalize(() => {
this.hideLoader();
})
);
}
}


ngOnDestroy(): void {
this.subscribedObservable$.unsubscribe();
}


/**
* @description Hides loader when all request gets complete
*/
private hideLoader() {
this.activeRequests--;
if (this.activeRequests === 0) {
this.loaderService.loadLoader.next(false);
}
}


/**
* @description set new auth token by existing refresh token
*/
private refreshToken() {
if (this.refreshTokenInProgress) {
return new Observable(observer => {
this.subscribedObservable$.add(
this.tokenRefreshed$.subscribe(() => {
observer.next();
observer.complete();
})
);
});
} else {
this.refreshTokenInProgress = true;


return this.authenticationService.getNewAccessTokenByRefreshToken().pipe(tap(newAuthToken => {
this.authenticationService.updateAccessToken(newAuthToken.access_token);
this.refreshTokenInProgress = false;
this.tokenRefreshedSource.next();
}));
}
}


private addAuthHeader(request: HttpRequest<any>) {
const accessToken = this.authenticationService.getAccessTokenOnly();
return request.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`
}
});
}


/**
* @todo move in common service or auth service once tested
* logout and redirect to login
*/
private logout() {
this.authenticationService.removeSavedUserDetailsAndLogout();
}

我必须解决以下要求:

  • Something 对于多个请求只刷新令牌一次
  • Something 如果刷新令牌失败,则注销用户
  • ✅ Log out if user gets an error after first refreshing
  • Something 在刷新令牌时对所有请求进行排队

因此,我收集了不同的选项来刷新角度令牌:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let retries = 0;
return this.authService.token$.pipe(
map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
concatMap(authReq => next.handle(authReq)),
// Catch the 401 and handle it by refreshing the token and restarting the chain
// (where a new subscription to this.auth.token will get the latest token).
catchError((err, restart) => {
// If the request is unauthorized, try refreshing the token before restarting.
if (err.status === 401 && retries === 0) {
retries++;
    

return concat(this.authService.refreshToken$, restart);
}
    

if (retries > 0) {
this.authService.logout();
}
    

return throwError(err);
})
);
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.token$.pipe(
map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
concatMap(authReq => next.handle(authReq)),
retryWhen((errors: Observable<any>) => errors.pipe(
mergeMap((error, index) => {
// any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen
if (error.status !== 401) {
return throwError(error);
}
    

if (index === 0) {
// first time execute refresh token logic...
return this.authService.refreshToken$;
}
    

this.authService.logout();
return throwError(error);
}),
take(2)
// first request should refresh token and retry,
// if there's still an error the second time is the last time and should navigate to login
)),
);
}

所有这些选项都经过了大量的测试,可以在 角刷新令牌 github 回购中找到

参见:

在 Andrei Ostrovski 提出的最为广泛接受的 回答中,当令牌刷新请求由于某种原因失败时,人们会对内存泄漏进行评论。可以通过使用 RxJS 超时操作符来减轻这种情况,如下所示:

//...


tokenRefreshTimeout = 60000;


//...


// Invalid token error
else if (error.status === 401) {
return this.refreshToken().pipe(
timeout(this.tokenRefreshTimeout), //added timeout here
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
//...

(对不起,我没有足够的名声来评论,我也不能建议编辑,因为编辑队列总是满的)

我的回答

在这种情况下,只需处理程序401

@Injectable()
export class AuthHttpInterceptor implements HttpInterceptor {


logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());


constructor(private authService: AuthService) { }


private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
});
}


public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (InterceptorSkipHeader.checkHeader(request)) {
const req = InterceptorSkipHeader.deleteHeader(request);
return next.handle(req);
}
const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
}


httpErrorsHandler() {
return (source$: Observable<any>) => source$.pipe(
catch401Error(() => this.handle401Error(source$)),
catch400Error((err) => EMPTY),
catch403Error((err) => EMPTY),
catch406Error((err) => EMPTY),
catch500Error((err) => EMPTY),
);
}


handle401Error(retry$: Observable<any>): Observable<any> {
return retry$.pipe(
startWhen(this.refresh$),
takeUntil(this.authService.logout$),
catch401Error(() => this.logoutUser$),
);
}
}

完整代码 (auth-http-拦截器. ts)

step 1, Create two Observable

返回文章页面

  • use defer() do your logout logic (like clear token from LocalStorage) and retun EMPTY

返回文章页面

  • use defer create refresh$ Observable, make it always take new refresh token to call refresh API

  • logout on catch error

  • share()这个可观察的(使所有401等待相同的刷新 API 回来)

logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());

第二步,截击机

just make api skip interceptor ( 学校 )

class Xheader {
static readonly interceptorSkipHeader = new Xheader('interceptorSkipHeader');


readonly headers = { [this.headerName]: this.headerName };
readonly options = { headers: this.headers };


private constructor(readonly headerName: string) { }


public checkHeader({ headers }: HttpRequest<any>) {
return headers.has(this.headerName);
}


public deleteHeader(request: HttpRequest<any>) {
return request.clone({ headers: request.headers.delete(this.headerName) });
}
}


export const InterceptorSkipHeader = Xheader.interceptorSkipHeader;

like this InterceptorSkipHeader.options ( Auth.service.ts)

refreshTokenFromServer(): Observable<Token> {
return this.http.post<Token>(this.authApi + '/refreshToken', this.token, InterceptorSkipHeader.options).pipe(setTokenToLocalStorage());
}

第三步,拦截者

4

跳过头 InterceptorSkipHeader.checkHeader(request)

  • 没有处理程序的删除和返回

否则,联络人

  1. 使用访问令牌创建 nextHandle$: applyCredentials(request)使用 defer()(总是使用新的访问令牌)
  2. 使用 iif()检查令牌是否为空将 logoutUser$,否则 nextHandle$
  3. add httpErrorsHandler() operator, handler this stream
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (InterceptorSkipHeader.checkHeader(request)) {
const req = InterceptorSkipHeader.deleteHeader(request);
return next.handle(req);
}
const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
}

添加访问令牌功能

private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
});
}

第四步,自定义操作员

我们应该在错误处理程序之前创建一些自定义操作符

运算符 catchHttpError

在这种情况下,我们只是处理401

  • 捕捉 http://http://http://http://http://http://http://http://http://http://
  • 捕捉到 http://http://http://http://http://http://http://http://http://http://http://http://
  • 抓住 http://http://http://http://http://http://http://http://http://http://
  • catch406Error : catch http 406
  • 抓住 http 500
function catchHttpError(...status: Array<number>) {
const statusMap = status.reduce((m, v) => m.set(v, v), new Map());
return (next: (err: HttpErrorResponse) => Observable<any>) => {
return catchError((err) => err instanceof HttpErrorResponse && statusMap.has(err.status) ? next(err) : throwError(err));
};
}


const catch401Error = catchHttpError(401);
const catch400Error = catchHttpError(400);
const catch403Error = catchHttpError(403);
const catch406Error = catchHttpError(406);
const catch500Error = catchHttpError(500);

StartWhen 运算符(学校)

equal delayWhen() second parameter (subscriptionDelay)

export function startWhen<T>(subscriptionDelay: Observable<any>) {
return (source$: Observable<T>) => concat(subscriptionDelay.pipe(take(1), ignoreElements()), source$);
}

步骤5,Http 错误处理程序

5

在这种情况下,我们只是处理401

Catch401Error 必须是第一个 (确保其他错误处理程序将捕获重试 API 错误)

  • handle401Error(source$)将重试 source$(以前的可观测值)
httpErrorsHandler() {
return (source$: Observable<any>) => source$.pipe(
catch401Error(() => this.handle401Error(source$)),
catch400Error((err) => EMPTY),
catch403Error((err) => EMPTY),
catch406Error((err) => EMPTY),
catch500Error((err) => EMPTY),
);
}

handle401Error

  • startWhen() : retry$ will wait refresh$ complete than call retry API
  • 在过程中,如果 authService.logout$触发器将停止流(取消订阅)
  • 如果重试 API 仍有401错误将注销用户
handle401Error(retry$: Observable<any>): Observable<any> {
return retry$.pipe(
startWhen(this.refresh$),
takeUntil(this.authService.logout$),
catch401Error(() => this.logoutUser$),
);
}

Https://medium.com/@eddylin1937/angular-interceptor-with-rxjs-refresh-token-176326c84a36

在使用 HTTP Error 401失败的 api 之后,令牌刷新 api 被调用,所有失败的和缓存的请求都可以使用 HTTP 拦截器重试。

if (this.isRefreshingToken && !req.url.endsWith(tokenURL)) {
// check if unique url to be added in cachedRequest


if (urlPresentIndex == -1) {
this.cachedRequests.push(req);
return this.tokenSubject.pipe(
switchMap(() => next.handle(req)),
tap((v) => {
// delete request from catchedRequest if api gets called


this.cachedRequests.splice(
this.cachedRequests.findIndex(
(httpRequest) => httpRequest.url == req.url
),
1
);
return EMPTY;
})
);
} else {
//already in cached request array


return EMPTY;
}
}

更多的细节,你可以阅读我的中等文章 令牌-刷新-拦截器-重试-请求失败

看看它是如何工作的 Stackblitz