Javascript 令牌刷新后的Angular 4拦截器重试请求
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/45202208/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Angular 4 Interceptor retry requests after token refresh
提问by Kovaci
Hi I am trying to figure out how implement the new angular interceptors and handle 401 unauthorizederrors by refreshing the token and retrying the request. This is the guide I have been following: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
嗨,我想弄清楚如何401 unauthorized通过刷新令牌并重试请求来实现新的角度拦截器和处理错误。这是我一直在遵循的指南:https: //ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
I am successfully caching the failed requests and can refresh the token but I cannot figure out how to resend the requests that previously failed. I also want to get this to work with the resolvers I am currently using.
我成功缓存了失败的请求并且可以刷新令牌,但我不知道如何重新发送以前失败的请求。我还想让它与我目前使用的解析器一起使用。
token.interceptor.ts
token.interceptor.ts
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();
}
} );
}
}
} );
authentication.service.ts
身份验证.service.ts
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
} );
}
The above retryFailedRequests() file is what I can't figure out. How do I resend the requests and make them available to the route through the resolver after retrying?
上面的 retryFailedRequests() 文件是我无法弄清楚的。重试后如何通过解析器重新发送请求并使它们可用于路由?
This is all the relevant code if that helps: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
如果有帮助,这是所有相关代码:https: //gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
回答by Andrei Ostrovski
My final solution. Works with parallel requests.
我的最终解决方案。适用于并行请求。
UPDATE:The code updated with Angular 9 / RxJS 6, error handling and fix looping when refreshToken fails
更新:使用 Angular 9 / RxJS 6 更新的代码,错误处理和修复 refreshToken 失败时的循环
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
};
回答by Samarpan
With the latest version of Angular (7.0.0) and rxjs (6.3.3), this is how I created a fully functional Auto Session recovery interceptor ensuring, if concurrent requests fail with 401, then also, it should only hit token refresh API once and pipe the failed requests to the response of that using switchMap and Subject. Below is how my interceptor code looks like. I have omitted the code for my auth service and store service as they are pretty standard service classes.
使用最新版本的 Angular (7.0.0) 和 rxjs (6.3.3),这就是我创建一个功能齐全的自动会话恢复拦截器的方式,确保如果并发请求因 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;
}
}
As per @anton-toshik comment, I thought it's a good idea to explain the functioning of this code in a write-up. You can have a read at my article herefor the explanation and understanding of this code (how and why it works?). Hope it helps.
根据@anton-toshik 的评论,我认为在一篇文章中解释这段代码的功能是个好主意。您可以在这里阅读我的文章以解释和理解这段代码(它是如何工作的以及为什么工作?)。希望能帮助到你。
回答by rdukeshier
I ran into a similar problem as well and I think the collect/retry logic is overly complicated. Instead, we can just use the catch operator to check for the 401, then watch for the token refresh, and rerun the request:
我也遇到了类似的问题,我认为收集/重试逻辑过于复杂。相反,我们可以只使用 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;
}
回答by James Lieu
Andrei Ostrovski's final solution works really well, but does not work if the refresh token is also expired (assuming you're making an api call to refresh). After some digging, I realised that the refresh token API call was also intercepted by the interceptor. I've had to add an if statement to handle this.
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 );
});
}
回答by Thanh Nhan
Based on this example, here's my piece
基于这个例子,这是我的作品
@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;
}
}
You may want to check if user enabled Remember Meto use refresh token for retrying or just redirect to logout page.
您可能需要检查用户是否Remember Me可以使用刷新令牌进行重试或仅重定向到注销页面。
Fyi, the LoginServicehas the following methods:
- getAccessToken(): string - return the current access_token
- isRememberMe(): boolean - check if user have refresh_token
- refreshToken(): Observable / Promise - Request to oauth server for new access_tokenusing refresh_token
- invalidateSession(): void - remove all user info and redirect to logout page
仅供参考,LoginService有以下方法:
- getAccessToken(): string - 返回当前access_token
- isRememberMe(): boolean - 检查用户是否拥有refresh_token
- refreshToken(): Observable / Promise - 请求 oauth 服务器进行新的access_token使用refresh_token
- invalidateSession(): void - 删除所有用户信息并重定向到注销页面
回答by Lahar Shah
Ideally, you want to check isTokenExpiredbefore request sent. And if expired refresh the token and add refreshed in the header.
理想情况下,您希望isTokenExpired在发送请求之前进行检查。如果过期刷新令牌并在标题中添加刷新。
Other than that retry operatormay help with your logic of refreshing token on 401 response.
除此之外,retry operator可能有助于您在 401 响应中刷新令牌的逻辑。
Use the RxJS retry operatorin your service where you are making a request. It accepts a retryCountargument.
If not provided, it will retry the sequence indefinitely.
RxJS retry operator在您提出请求的服务中使用。它接受一个retryCount参数。如果未提供,它将无限期地重试序列。
In your interceptor on response refresh the token and return the error. When your service gets back the error but now retry operator is being used so it will retry the request and this time with the refreshed token(Interceptor uses refreshed token to add in the header.)
在响应拦截器中刷新令牌并返回错误。当您的服务返回错误但现在正在使用重试运算符时,它将重试请求,这次使用刷新的令牌(拦截器使用刷新的令牌添加到标头中。)
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);
}
}
回答by Saurabh Deshmukh
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();
}
回答by Johseffer Chepli
I got this creating a new request based on the url of the failed request and sending the same body of the failed request.
我得到了这个基于失败请求的 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();
}
}
回答by Attrox_
In your authentication.service.ts, you should have a HttpClient injected as a dependency
在您的 authentication.service.ts 中,您应该将 HttpClient 作为依赖项注入
constructor(private http: HttpClient) { }
You can then re-submit the request (inside retryFailedRequests) as follow:
然后您可以重新提交请求(在 retryFailedRequests 中),如下所示:
this.http.request(request).subscribe((response) => {
// You need to subscribe to observer in order to "retry" your request
});

