如何在 angular http 拦截器中以异步方式缓存 http 请求?
How to cache http requests in async style in angular http inteceptor?
我正在编写 angular 5 应用程序。 authentication service
中有refreshAccessToken
refreshAccessToken(): Observable<ICredentials> {
const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
const httpParams = new HttpParams()
.append('grant_type', 'refresh_token')
.append('refresh_token', this.credentials.refresh_token)
.append('client_id', Constants.CLIENT_ID)
.toString();
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(refreshTokenUrl, httpParams, { headers })
.map((response: any) => {
this.setCredentials(response);
localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
return response;
});
}
我想实现下一个算法:
- 任何 http 请求因未授权而失败,状态为 401
- 尝试从服务器获取新的访问令牌
- 重复请求
在获取新的访问令牌时,可以创建新的 http 请求,在这种情况下,我想存储它们并在收到新的访问令牌后重复。为了达到这个目的,我编写了拦截器。
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '@app/core/authentication/authentication.service';
import { Urls, UrlsService } from '@app/shared/urls';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class UnauthorizedRequestInterceptor implements HttpInterceptor {
newAccessToken$: Observable<ICredentials> = null;
constructor(
public authService: AuthenticationService,
private router: Router,
private urlsService: UrlsService) {
}
addAuthHeader(request: HttpRequest<any>) {
if (this.authService.getCredentials()) {
return request.clone({
setHeaders: {
'Authorization': 'Bearer ' + this.authService.getCredentials().access_token
}
});
}
return request;
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = this.addAuthHeader(request);
return next.handle(request).catch((error: HttpErrorResponse) => {
let handleRequests$ = null;
if (this.isNeedNewAccessToken(error, request)) {
handleRequests$ = this.handleRequestWithNewAccessToken(request, next);
}
return handleRequests$ ||
(this.isUnathorizedError(error)
? Observable.empty()
: Observable.throw(error));
});
}
logout() {
this.authService.logout();
this.router.navigate(['login']);
}
private isNeedNewAccessToken(error: HttpErrorResponse, request: HttpRequest<any>): boolean {
return this.isUnathorizedError(error)
&& this.authService.isAuthenticated()
&& this.isSignInRequest(request);
}
private getNewAccessToken(): Observable<ICredentials> {
if (!this.newAccessToken$) {
this.newAccessToken$ = this.authService.refreshAccessToken();
}
return this.newAccessToken$;
}
private isUnathorizedError(error: HttpErrorResponse) {
return error.status === 401;
}
private handleRequestWithNewAccessToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
return this.getNewAccessToken()
.mergeMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
})
.catch((err: HttpErrorResponse) => {
if (err.error.error === 'invalid_grant') {
this.logout();
}
return Observable.empty();
});
}
private isNotSignInRequest(request: HttpRequest<any>): boolean {
return request.url !== this.urlsService.getUrl(Urls.TOKEN);
}
}
这个拦截器的行为真的很奇怪。在 handleRequestWithNewAccessToken
上的每个 mergeMap
上,angular 启动新的 post httpRequest。我预计从 refreshAccessToken
(来自 authenticationService
的函数,顶部的代码)返回的可观察对象只会被解析一次。我不明白为什么每个合并地图都会触发它?我期待下一个:
- 我有可观察的 - 令牌的 http 请求
- 我使用 mergeMap - 当 http 请求完成时,将执行所有使用 mergeMap 添加的回调。
我想把我需要处理的请求存储在全局变量中,并在 subscribe()
到 http 请求中调用它们,但是有问题,每个请求都应该在初始流中解析拦截器。我不能这样做:.subscribe(token => this.httpClient.request(storedRequest)
因为这会创建新请求,所以所有操作都应该在可观察链内发生。
你能帮我找到解决办法吗?
PS 此解决方案有效,但我想摆脱不必要的令牌请求,f.e。如果页面需要发出 5 次请求并且令牌已过期 - 拦截器将发出 5 次令牌请求。
我认为你的代码很好,你所要做的就是share
新令牌的请求。
refreshAccessToken(): Observable<ICredentials> {
const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
const httpParams = new HttpParams()
.append('grant_type', 'refresh_token')
.append('refresh_token', this.credentials.refresh_token)
.append('client_id', Constants.CLIENT_ID)
.toString();
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(refreshTokenUrl, httpParams, { headers })
.map((response: any) => {
this.setCredentials(response);
localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
return response;
})
.share(); // <- HERE
}
注意 share
运算符在 return
末尾
编辑:
我还认为您永远不会将 this.newAccessToken$
退回到 null
。也许考虑像这样将 null
添加到 finally
:
private getNewAccessToken(): Observable<ICredentials> {
if (!this.newAccessToken$) {
this.newAccessToken$ = this.authService.refreshAccessToken()
.finally(() => {
this.newAccessToken$ = null;
});
}
return this.newAccessToken$;
}
我正在编写 angular 5 应用程序。 authentication service
refreshAccessToken
refreshAccessToken(): Observable<ICredentials> {
const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
const httpParams = new HttpParams()
.append('grant_type', 'refresh_token')
.append('refresh_token', this.credentials.refresh_token)
.append('client_id', Constants.CLIENT_ID)
.toString();
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(refreshTokenUrl, httpParams, { headers })
.map((response: any) => {
this.setCredentials(response);
localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
return response;
});
}
我想实现下一个算法:
- 任何 http 请求因未授权而失败,状态为 401
- 尝试从服务器获取新的访问令牌
- 重复请求
在获取新的访问令牌时,可以创建新的 http 请求,在这种情况下,我想存储它们并在收到新的访问令牌后重复。为了达到这个目的,我编写了拦截器。
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '@app/core/authentication/authentication.service';
import { Urls, UrlsService } from '@app/shared/urls';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class UnauthorizedRequestInterceptor implements HttpInterceptor {
newAccessToken$: Observable<ICredentials> = null;
constructor(
public authService: AuthenticationService,
private router: Router,
private urlsService: UrlsService) {
}
addAuthHeader(request: HttpRequest<any>) {
if (this.authService.getCredentials()) {
return request.clone({
setHeaders: {
'Authorization': 'Bearer ' + this.authService.getCredentials().access_token
}
});
}
return request;
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = this.addAuthHeader(request);
return next.handle(request).catch((error: HttpErrorResponse) => {
let handleRequests$ = null;
if (this.isNeedNewAccessToken(error, request)) {
handleRequests$ = this.handleRequestWithNewAccessToken(request, next);
}
return handleRequests$ ||
(this.isUnathorizedError(error)
? Observable.empty()
: Observable.throw(error));
});
}
logout() {
this.authService.logout();
this.router.navigate(['login']);
}
private isNeedNewAccessToken(error: HttpErrorResponse, request: HttpRequest<any>): boolean {
return this.isUnathorizedError(error)
&& this.authService.isAuthenticated()
&& this.isSignInRequest(request);
}
private getNewAccessToken(): Observable<ICredentials> {
if (!this.newAccessToken$) {
this.newAccessToken$ = this.authService.refreshAccessToken();
}
return this.newAccessToken$;
}
private isUnathorizedError(error: HttpErrorResponse) {
return error.status === 401;
}
private handleRequestWithNewAccessToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
return this.getNewAccessToken()
.mergeMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
})
.catch((err: HttpErrorResponse) => {
if (err.error.error === 'invalid_grant') {
this.logout();
}
return Observable.empty();
});
}
private isNotSignInRequest(request: HttpRequest<any>): boolean {
return request.url !== this.urlsService.getUrl(Urls.TOKEN);
}
}
这个拦截器的行为真的很奇怪。在 handleRequestWithNewAccessToken
上的每个 mergeMap
上,angular 启动新的 post httpRequest。我预计从 refreshAccessToken
(来自 authenticationService
的函数,顶部的代码)返回的可观察对象只会被解析一次。我不明白为什么每个合并地图都会触发它?我期待下一个:
- 我有可观察的 - 令牌的 http 请求
- 我使用 mergeMap - 当 http 请求完成时,将执行所有使用 mergeMap 添加的回调。
我想把我需要处理的请求存储在全局变量中,并在 subscribe()
到 http 请求中调用它们,但是有问题,每个请求都应该在初始流中解析拦截器。我不能这样做:.subscribe(token => this.httpClient.request(storedRequest)
因为这会创建新请求,所以所有操作都应该在可观察链内发生。
你能帮我找到解决办法吗?
PS 此解决方案有效,但我想摆脱不必要的令牌请求,f.e。如果页面需要发出 5 次请求并且令牌已过期 - 拦截器将发出 5 次令牌请求。
我认为你的代码很好,你所要做的就是share
新令牌的请求。
refreshAccessToken(): Observable<ICredentials> {
const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
const httpParams = new HttpParams()
.append('grant_type', 'refresh_token')
.append('refresh_token', this.credentials.refresh_token)
.append('client_id', Constants.CLIENT_ID)
.toString();
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(refreshTokenUrl, httpParams, { headers })
.map((response: any) => {
this.setCredentials(response);
localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
return response;
})
.share(); // <- HERE
}
注意 share
运算符在 return
编辑:
我还认为您永远不会将 this.newAccessToken$
退回到 null
。也许考虑像这样将 null
添加到 finally
:
private getNewAccessToken(): Observable<ICredentials> {
if (!this.newAccessToken$) {
this.newAccessToken$ = this.authService.refreshAccessToken()
.finally(() => {
this.newAccessToken$ = null;
});
}
return this.newAccessToken$;
}