rxJs & angular 4 & restangular: 堆栈错误拦截器
rxJs & angular 4 & restangular: stack errorInterceptors
在我的 angular 4 应用程序中,我使用 ngx-restangular
来处理所有服务器调用。结果 returns 可观察到,并且此模块具有处理错误(如 401 等)的挂钩。
但是根据文档,我可以处理 403 (401) 所以:
RestangularProvider.addErrorInterceptor((response, subject, responseHandler) => {
if (response.status === 403) {
refreshAccesstoken()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
return false; // error handled
}
return true; // error not handled
});
这适用于一个请求,该请求因 403 错误而中断。
我如何使用 rxJs 堆叠此调用?因为现在,例如,我有 3 个请求,其中有 403 个,对于每个这个损坏的请求,我正在刷新令牌 - 这不是很好,我必须更新我的令牌,然后重复我所有损坏的请求。我怎样才能使用 Observables 实现这个目标?
在 angular 1 中非常简单:
Restangular.setErrorInterceptor(function (response, deferred, responseHandler) {
if (response.status == 403) {
// send only one request if multiple errors exist
if (!refreshingIsInProgress) {
refreshingIsInProgress = AppApi.refreshAccessToken(); // Returns promise
}
$q.when(refreshingIsInProgress, function () {
refreshingIsInProgress = null;
setHeaders(response.config.headers);
// repeat request with error
$http(response.config).then(responseHandler, deferred);
}, function () {
refreshingIsInProgress = null;
$state.go('auth');
});
return false; // stop the promise chain
}
return true;
});
一切都很顺利。但是我是 rxJs & angular 4 的新手,我不知道如何使用可观察对象和 angular 4 来实现这一目标。也许有人有想法?
更新!
这是我的 refreshAccesstoken 方法
const refreshAccesstoken = function () {
const refreshToken = http.post(environment.apiURL + `/token/refresh`,
{refreshToken: 'someToken'});
return refreshToken;
};
我在我的应用程序中遇到了类似的问题,我是这样解决的:
Subscribers keep waiting for source to emit a value and dont do anything till then
我们可以使用这种行为来解决我们的问题。
我们能做的是这样的:
所有 HTTP 调用:
public doSomeHttpCall(params){
return authService
.getToken()
.switchMap(token => httpcall())
}
使用上面的代码,在我们获得令牌之前不会发生 HTTP 调用。
内部身份验证服务:
private token = new BehaviorSubject(null);
public getToken(){
return this.token
.filter( t => !!t );
}
因为我有一个令牌过滤器,如果令牌是 falsy
,则不会执行 HTTP 调用。一旦令牌获得合适的值,所有 HTTP 调用就会继续执行。
在您的情况下,如果有 403
,请通过将其设置为 null 来禁用该令牌。刷新后将其设置为新的令牌值。
private refreshInProgress= false;
public refreshToken(){
if(refreshInProgress){
//dont do anything
return;
}
refreshInProgress = true;
this.token.next(null);
// fetch new token
this.token.next(newToken);
refreshInProgress = false;
}
这里有一个非常简单的例子:
jsbin : https://jsbin.com/mefepipiqu/edit?html,js,console,output
我的解决方案涉及扩展 angular 提供的 HTTP 服务并覆盖方法以添加以下行为:
- 像这样为所有 HTTP 调用添加重试逻辑:
post<T>(serviceUrl: string, data: any): Observable<T> {
return Observable.defer(() => {
return super.post<T>(serviceUrl, data);
}).retryWhen((error) => {
return this.refresh(error);
});
}
- 如果状态码与403.Else不同则抛出刷新令牌并重试。你会尝试这样做,比方说 3 次。之后你就可以放弃了:)
refresh(obs: Observable<any>): Observable<any> {
return obs
.switchMap((x: any) => {
if (x.status === 403) {
return Observable.of(x);
}
return Observable.throw(x);
})
.scan((acc, value) => {
return acc + 1;
}, 0)
.takeWhile(acc => acc < 3)
.flatMap(() => {
console.log('Token refresh retry');
return this.tokenRefreshService.refreshToken();
});
}
我可以看到使用 ngx-restangular 执行此操作的一种方法是使用 share 运算符。这样您就不必实现复杂的排队逻辑。这个想法是,如果你有 3 个请求都带有 403 响应,它们都会命中你的拦截器并调用你的可观察对象。如果您共享该可观察对象,对于 3 个令牌损坏的请求,您将只有一个令牌请求。
您只需像这样在代码中使用共享运算符:
refreshAccesstoken()
.share()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
我还没有检查过代码是否真的有效,但我之前在相同的用例中使用过这种方法,但我使用的不是拦截器,而是 angular HTTP 服务。
编辑以更改 refreshAccessToken:
您需要将 refreshAccessToken 方法包装在延迟的 Observable 中并共享它。这样你每次都会重复使用相同的可观察对象。
在构造函数中:
this.source = Observable.defer(() => {
return this.refreshAccesstoken();
}).share();
创建另一个将调用该可观察对象的方法:
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
EDIT2
我创建了一个 git repository,它使用 angular2 和 restangular。
场景如下:
- 在我的 app.component 中,我发出 3 个并发请求以获取订单列表。请求完成后,我将记录 "Orders received".
- orders 端点需要身份验证令牌。如果未提供,它将 return 401。
- 在我的 app.module 中,我仅将基数 URL 设置为我的 API。因为我也没有设置授权令牌,所以我的所有请求都将失败并返回 401。
- 执行拦截器代码时,它会设置刷新令牌,在我的例子中,它是硬编码在请求中并重复请求的。
- 每次执行时 return 标记的可观察记录 "Getting token"。
这是我在控制台中看到的内容:
如果我删除共享运算符,我将获得以下日志:
这意味着每次都会创建可观察对象。
为了使其工作,重要的是在 RestangularConfigFactory 中声明和创建源。它本质上将成为一个单例对象,这就是允许 Share 运算符工作的原因。
注意:
我为这个项目创建了一个简单的网络 API 在本地托管,因为它对我来说速度更快。
EDIT3:更新以包含刷新令牌的代码:
@Injectable()
export class TokenRefreshService {
source: Observable<any>;
pausedObservable: Observable<any>;
constructor(
private authenthicationStore: AuthenticationStore,
private router: Router,
private authenticationDataService: AuthenticationDataService,
private http: ObservableHttpService) {
this.source = Observable.defer(() => {
return this.postRequest();
}).share();
}
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
public shouldRefresh(): boolean {
if (this.getTime() < 0) {
return true;
}
return false;
}
private postRequest(): Observable<any> {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData == null) {
return Observable.empty();
}
let data: URLSearchParams = new URLSearchParams();
data.append('grant_type', 'refresh_token');
let obs = this.http.postWithHeaders(
'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
.map((response) => {
return this.parseResult(true, response, 'authenticateUserResult');
})
.catch((error) => {
let errorMessage = this.rejected(error);
return Observable.throw(errorMessage);
});
return obs;
}
private rejected(failure) {
let authenticateUserResult;
let response = failure;
let data = response.json();
if (response &&
response.status === 400 &&
data &&
data.error &&
data.error === 'invalid_grant') {
authenticateUserResult = this.parseResult(false, data, 'error_description');
return authenticateUserResult;
} else {
return failure;
}
}
private parseResult(success, data, resultPropertyName) {
let authenticateResultParts = data[resultPropertyName].split(':');
data.result = {
success: success,
id: authenticateResultParts[0],
serverDescription: authenticateResultParts[1]
};
return data;
}
private resolved(data): void {
let authenticationResult = data.result;
if (authenticationResult && authenticationResult.success) {
let authenticationData = this.createAuthenticationData(data);
this.authenthicationStore.setUserData(authenticationData);
} else {
this.authenthicationStore.clearAll();
this.router.navigate(['/authenticate/login']);
}
}
private createAuthenticationData(data: any): AuthenticationData {
let authenticationData = new AuthenticationData();
authenticationData.access_token = data.access_token;
authenticationData.token_type = data.token_type;
authenticationData.username = data.username;
authenticationData.friendlyName = data.friendlyName;
return authenticationData;
}
private getTime(): number {
return this.getNumberOfSecondsBeforeTokenExpires(this.getTicksUntilExpiration());
}
private getTicksUntilExpiration(): number {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData) {
return authData.expire_time;
}
return 0;
}
private getNumberOfSecondsBeforeTokenExpires(ticksWhenTokenExpires: number): number {
let a;
if (ticksWhenTokenExpires === 0) {
a = new Date(new Date().getTime() + 1 * 60000);
} else {
a = new Date((ticksWhenTokenExpires) * 1000);
}
let b = new Date();
let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate(), a.getHours(), a.getMinutes(), a.getSeconds());
let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate(), b.getHours(), b.getMinutes(), b.getSeconds());
let timeInSeconds = Math.floor((utc1 - utc2) / 1000);
return timeInSeconds - 5;
}
}
RxJs 不是灵丹妙药,在这种情况下,它会使事情变得更加复杂。
虽然可以组合多个运算符并在单个可观察流中包含 if 块、重试块,但最终解决方案将过于复杂,难以支持、理解和重构。对于 http 处理尤其如此。
在我的 angular 4 应用程序中,我使用 ngx-restangular
来处理所有服务器调用。结果 returns 可观察到,并且此模块具有处理错误(如 401 等)的挂钩。
但是根据文档,我可以处理 403 (401) 所以:
RestangularProvider.addErrorInterceptor((response, subject, responseHandler) => {
if (response.status === 403) {
refreshAccesstoken()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
return false; // error handled
}
return true; // error not handled
});
这适用于一个请求,该请求因 403 错误而中断。 我如何使用 rxJs 堆叠此调用?因为现在,例如,我有 3 个请求,其中有 403 个,对于每个这个损坏的请求,我正在刷新令牌 - 这不是很好,我必须更新我的令牌,然后重复我所有损坏的请求。我怎样才能使用 Observables 实现这个目标?
在 angular 1 中非常简单:
Restangular.setErrorInterceptor(function (response, deferred, responseHandler) {
if (response.status == 403) {
// send only one request if multiple errors exist
if (!refreshingIsInProgress) {
refreshingIsInProgress = AppApi.refreshAccessToken(); // Returns promise
}
$q.when(refreshingIsInProgress, function () {
refreshingIsInProgress = null;
setHeaders(response.config.headers);
// repeat request with error
$http(response.config).then(responseHandler, deferred);
}, function () {
refreshingIsInProgress = null;
$state.go('auth');
});
return false; // stop the promise chain
}
return true;
});
一切都很顺利。但是我是 rxJs & angular 4 的新手,我不知道如何使用可观察对象和 angular 4 来实现这一目标。也许有人有想法?
更新! 这是我的 refreshAccesstoken 方法
const refreshAccesstoken = function () {
const refreshToken = http.post(environment.apiURL + `/token/refresh`,
{refreshToken: 'someToken'});
return refreshToken;
};
我在我的应用程序中遇到了类似的问题,我是这样解决的:
Subscribers keep waiting for source to emit a value and dont do anything till then
我们可以使用这种行为来解决我们的问题。
我们能做的是这样的:
所有 HTTP 调用:
public doSomeHttpCall(params){
return authService
.getToken()
.switchMap(token => httpcall())
}
使用上面的代码,在我们获得令牌之前不会发生 HTTP 调用。
内部身份验证服务:
private token = new BehaviorSubject(null);
public getToken(){
return this.token
.filter( t => !!t );
}
因为我有一个令牌过滤器,如果令牌是 falsy
,则不会执行 HTTP 调用。一旦令牌获得合适的值,所有 HTTP 调用就会继续执行。
在您的情况下,如果有 403
,请通过将其设置为 null 来禁用该令牌。刷新后将其设置为新的令牌值。
private refreshInProgress= false;
public refreshToken(){
if(refreshInProgress){
//dont do anything
return;
}
refreshInProgress = true;
this.token.next(null);
// fetch new token
this.token.next(newToken);
refreshInProgress = false;
}
这里有一个非常简单的例子:
jsbin : https://jsbin.com/mefepipiqu/edit?html,js,console,output
我的解决方案涉及扩展 angular 提供的 HTTP 服务并覆盖方法以添加以下行为:
- 像这样为所有 HTTP 调用添加重试逻辑:
post<T>(serviceUrl: string, data: any): Observable<T> {
return Observable.defer(() => {
return super.post<T>(serviceUrl, data);
}).retryWhen((error) => {
return this.refresh(error);
});
}
- 如果状态码与403.Else不同则抛出刷新令牌并重试。你会尝试这样做,比方说 3 次。之后你就可以放弃了:)
refresh(obs: Observable<any>): Observable<any> {
return obs
.switchMap((x: any) => {
if (x.status === 403) {
return Observable.of(x);
}
return Observable.throw(x);
})
.scan((acc, value) => {
return acc + 1;
}, 0)
.takeWhile(acc => acc < 3)
.flatMap(() => {
console.log('Token refresh retry');
return this.tokenRefreshService.refreshToken();
});
}
我可以看到使用 ngx-restangular 执行此操作的一种方法是使用 share 运算符。这样您就不必实现复杂的排队逻辑。这个想法是,如果你有 3 个请求都带有 403 响应,它们都会命中你的拦截器并调用你的可观察对象。如果您共享该可观察对象,对于 3 个令牌损坏的请求,您将只有一个令牌请求。
您只需像这样在代码中使用共享运算符:
refreshAccesstoken()
.share()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
我还没有检查过代码是否真的有效,但我之前在相同的用例中使用过这种方法,但我使用的不是拦截器,而是 angular HTTP 服务。
编辑以更改 refreshAccessToken:
您需要将 refreshAccessToken 方法包装在延迟的 Observable 中并共享它。这样你每次都会重复使用相同的可观察对象。
在构造函数中:
this.source = Observable.defer(() => {
return this.refreshAccesstoken();
}).share();
创建另一个将调用该可观察对象的方法:
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
EDIT2
我创建了一个 git repository,它使用 angular2 和 restangular。 场景如下:
- 在我的 app.component 中,我发出 3 个并发请求以获取订单列表。请求完成后,我将记录 "Orders received".
- orders 端点需要身份验证令牌。如果未提供,它将 return 401。
- 在我的 app.module 中,我仅将基数 URL 设置为我的 API。因为我也没有设置授权令牌,所以我的所有请求都将失败并返回 401。
- 执行拦截器代码时,它会设置刷新令牌,在我的例子中,它是硬编码在请求中并重复请求的。
- 每次执行时 return 标记的可观察记录 "Getting token"。
这是我在控制台中看到的内容:
如果我删除共享运算符,我将获得以下日志:
为了使其工作,重要的是在 RestangularConfigFactory 中声明和创建源。它本质上将成为一个单例对象,这就是允许 Share 运算符工作的原因。
注意:
我为这个项目创建了一个简单的网络 API 在本地托管,因为它对我来说速度更快。
EDIT3:更新以包含刷新令牌的代码:
@Injectable()
export class TokenRefreshService {
source: Observable<any>;
pausedObservable: Observable<any>;
constructor(
private authenthicationStore: AuthenticationStore,
private router: Router,
private authenticationDataService: AuthenticationDataService,
private http: ObservableHttpService) {
this.source = Observable.defer(() => {
return this.postRequest();
}).share();
}
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
public shouldRefresh(): boolean {
if (this.getTime() < 0) {
return true;
}
return false;
}
private postRequest(): Observable<any> {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData == null) {
return Observable.empty();
}
let data: URLSearchParams = new URLSearchParams();
data.append('grant_type', 'refresh_token');
let obs = this.http.postWithHeaders(
'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
.map((response) => {
return this.parseResult(true, response, 'authenticateUserResult');
})
.catch((error) => {
let errorMessage = this.rejected(error);
return Observable.throw(errorMessage);
});
return obs;
}
private rejected(failure) {
let authenticateUserResult;
let response = failure;
let data = response.json();
if (response &&
response.status === 400 &&
data &&
data.error &&
data.error === 'invalid_grant') {
authenticateUserResult = this.parseResult(false, data, 'error_description');
return authenticateUserResult;
} else {
return failure;
}
}
private parseResult(success, data, resultPropertyName) {
let authenticateResultParts = data[resultPropertyName].split(':');
data.result = {
success: success,
id: authenticateResultParts[0],
serverDescription: authenticateResultParts[1]
};
return data;
}
private resolved(data): void {
let authenticationResult = data.result;
if (authenticationResult && authenticationResult.success) {
let authenticationData = this.createAuthenticationData(data);
this.authenthicationStore.setUserData(authenticationData);
} else {
this.authenthicationStore.clearAll();
this.router.navigate(['/authenticate/login']);
}
}
private createAuthenticationData(data: any): AuthenticationData {
let authenticationData = new AuthenticationData();
authenticationData.access_token = data.access_token;
authenticationData.token_type = data.token_type;
authenticationData.username = data.username;
authenticationData.friendlyName = data.friendlyName;
return authenticationData;
}
private getTime(): number {
return this.getNumberOfSecondsBeforeTokenExpires(this.getTicksUntilExpiration());
}
private getTicksUntilExpiration(): number {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData) {
return authData.expire_time;
}
return 0;
}
private getNumberOfSecondsBeforeTokenExpires(ticksWhenTokenExpires: number): number {
let a;
if (ticksWhenTokenExpires === 0) {
a = new Date(new Date().getTime() + 1 * 60000);
} else {
a = new Date((ticksWhenTokenExpires) * 1000);
}
let b = new Date();
let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate(), a.getHours(), a.getMinutes(), a.getSeconds());
let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate(), b.getHours(), b.getMinutes(), b.getSeconds());
let timeInSeconds = Math.floor((utc1 - utc2) / 1000);
return timeInSeconds - 5;
}
}
RxJs 不是灵丹妙药,在这种情况下,它会使事情变得更加复杂。
虽然可以组合多个运算符并在单个可观察流中包含 if 块、重试块,但最终解决方案将过于复杂,难以支持、理解和重构。对于 http 处理尤其如此。