我正在尝试在收到角度 7 的错误 401 时自动执行刷新令牌请求。
我没有找到太多关于如何使用 Angular 7 进行操作的文档,而且我之前没有 Angular 或 RxJS 的知识,我变得有点疯狂
我认为它几乎完成了,但由于某种原因,第二个 next.handle(newReq) 不发送请求(在 google chrome 网络调试器中仅出现第一个请求)
我正在获取刷新响应并使 processLoginResponse(res) 正确
你可以在这里看到我的拦截器
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let newReq = req.clone();
return next.handle(req).pipe(
catchError(error => {
if (error.status == 401) {
this._authenticationService.refresh().subscribe(
res => {
this._authenticationService.processLoginResponse(res);
newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
return next.handle(newReq)
},
error => {
this._authenticationService.logOut();
});
}
throw error;
})
);
你必须区分所有的请求。例如,您不想拦截您的登录请求,也不想拦截刷新令牌请求。 SwitchMap 是你最好的朋友,因为你需要取消一些调用来等待你的令牌刷新。
因此,您要做的就是首先检查状态 401(未经授权)的错误响应:
return next.handle(this.addToken(req, this.userService.getAccessToken()))
.pipe(catchError(err => {
if (err instanceof HttpErrorResponse) {
// token is expired refresh and try again
if (err.status === 401) {
return this.handleUnauthorized(req, next);
}
// default error handler
return this.handleError(err);
} else {
return observableThrowError(err);
}
}));
在你的handleUnauthorized函数中,你必须刷新你的令牌,同时跳过所有进一步的请求:
handleUnauthorized (req: HttpRequest<any>, next: HttpHandler): Observable<any> {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
// get a new token via userService.refreshToken
return this.userService.refreshToken()
.pipe(switchMap((newToken: string) => {
// did we get a new token retry previous request
if (newToken) {
this.tokenSubject.next(newToken);
return next.handle(this.addToken(req, newToken));
}
// If we don't get a new token, we are in trouble so logout.
this.userService.doLogout();
return observableThrowError('');
})
, catchError(error => {
// If there is an exception calling 'refreshToken', bad news so logout.
this.userService.doLogout();
return observableThrowError('');
})
, finalize(() => {
this.isRefreshingToken = false;
})
);
} else {
return this.tokenSubject
.pipe(
filter(token => token != null)
, take(1)
, switchMap(token => {
return next.handle(this.addToken(req, token));
})
);
}
}
我们在拦截器类上有一个属性,用于检查是否已经有刷新令牌请求正在运行:
this.isRefreshingToken = true;
因为当您触发多个未经授权的请求时,您不希望有多个刷新请求。
因此,
if (!this.isRefreshingToken)
部分中的所有内容都是关于刷新您的令牌并再次尝试之前的请求。
else
中处理的所有内容都适用于所有请求,同时,当您的 userService 刷新令牌时,会返回 tokenSubject,当令牌准备好时 this.tokenSubject.next(newToken);
将重试每个跳过的请求。
本文是拦截器的原始灵感:https://www.intertech.com/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/
编辑:
TokenSubject 实际上是一个行为主体:
tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
,这意味着任何新订阅者都将获得流中的当前值,这将是我们上次调用 this.tokenSubject.next(newToken)
时的旧令牌。
有了
next(null)
,每个新订阅者都不会触发switchMap
部分,这就是为什么filter(token => token != null)
是必要的。
使用新令牌再次调用
this.tokenSubject.next(newToken)
后,每个订阅者都会使用新令牌触发 switchMap
部分。希望现在更清楚了
下面是调用刷新令牌的代码,获取刷新令牌后调用失败的 API,
源代码中的注释将帮助您理解流程。 经过测试,适用于以下场景
1) 如果单个请求由于 401 失败,则会调用刷新 令牌并将使用更新的令牌调用失败的 API。
2)如果多个请求由于401而失败,则会调用刷新 令牌并将使用更新的令牌调用失败的 API。
3)不会重复调用token API
如果仍然有人发现此代码不起作用的新场景,请通知我,以便我进行相应的测试和更新。
import { Injectable } from "@angular/core";
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse, HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, tap, filter, take, finalize } from 'rxjs/operators';
import { TOKENAPIURL } from 'src/environments/environment';
import { SessionService } from '../services/session.service';
import { AuthService } from '../services/auth.service';
/**
* @author Pravin P Patil
* @version 1.0
* @description Interceptor for handling requests which giving 401 unauthorized and will call for
* refresh token and if token received successfully it will call failed (401) api again for maintaining the application momentum
*/
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private http: HttpClient, private sessionService: SessionService, private authService: AuthService) { }
/**
*
* @param request HttpRequest
* @param next HttpHandler
* @description intercept method which calls every time before sending requst to server
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Taking an access token
const accessToken = sessionStorage.getItem('ACCESS_TOKEN');
// cloing a request and adding Authorization header with token
request = this.addToken(request, accessToken);
// sending request to server and checking for error with status 401 unauthorized
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
// calling refresh token api and if got success extracting token from response and calling failed api due to 401
return this.handle401Error(request, next);
} // If api not throwing 401 but gives an error throwing error
else {
return throwError(error);
}
}));
}
/**
*
* @param request HttpRequest<any>
* @param token token to in Authorization header
*/
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: { 'Authorization': `Bearer ${token}` }
});
}
/**
* This method will called when any api fails due to 401 and calsl for refresh token
*/
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
// If Refresh token api is not already in progress
if (this.isRefreshing) {
// If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
// – which means the new token is ready and we can retry the request again
return this.refreshTokenSubject
.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt))
}));
} else {
// updating variable with api is in progress
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);
const refreshToken = sessionStorage.getItem('REFRESH_TOKEN');
// Token String for Refresh token OWIN Authentication
const tokenDataString = `refresh_token=${refreshToken}&grant_type=refresh_token`;
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'X-Skip-Interceptor': ''
})
};
return this.http.post<any>(TOKENAPIURL, tokenDataString, httpOptions)
.pipe(switchMap((tokens) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(tokens.access_token);
// updating value of expires in variable
sessionStorage.setItem('ACCESS_TOKEN', tokens.access_token);
return next.handle(this.addToken(request, tokens.access_token));
}));
}
}
}
你可以做这样的事情:
import { HttpErrorResponse } from '@angular/common/http';
return next.handle(req).pipe(
catchError((err: any) => {
if (err instanceof HttpErrorResponse && err.status 401) {
return this._authenticationService.refresh()
.pipe(tap(
(success) => {},
(err) => {
this._authenticationService.logOut();
throw error;
}
).mergeMap((res) => {
this._authenticationService.processLoginResponse(res);
newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
return next.handle(newReq)
});
} else {
return Observable.of({});
}
}
));