使用 rxjs 处理刷新令牌
Handling refresh tokens using rxjs
自从我开始使用 angular2 以来,我已经将我的服务设置为 return T 的 Observable。在该服务中,我将调用 map(),而使用这些服务的组件将只使用 subscribe()等待响应。对于这些简单的场景,我真的不需要深入研究 rxjs,所以一切都很好。
我现在想要实现以下目标:我正在使用带有刷新令牌的 Oauth2 身份验证。我想构建一个所有其他服务都将使用的 api 服务,并且当 returned 出现 401 错误时,它将透明地处理刷新令牌。因此,对于 401,我首先从 OAuth2 端点获取一个新令牌,然后使用新令牌重试我的请求。下面是工作正常的代码,有承诺:
request(url: string, request: RequestOptionsArgs): Promise<Response> {
var me = this;
request.headers = request.headers || new Headers();
var isSecureCall: boolean = true; //url.toLowerCase().startsWith('https://');
if (isSecureCall === true) {
me.authService.setAuthorizationHeader(request.headers);
}
request.headers.append('Content-Type', 'application/json');
request.headers.append('Accept', 'application/json');
return this.http.request(url, request).toPromise()
.catch(initialError => {
if (initialError && initialError.status === 401 && isSecureCall === true) {
// token might be expired, try to refresh token.
return me.authService.refreshAuthentication().then((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request).toPromise();
}
return <any>Promise.reject(initialError);
});
}
else {
return <any>Promise.reject(initialError);
}
});
}
在上面的代码中,authService.refreshAuthentication() 将获取新令牌并将其存储在 localStorage 中。 authService.setAuthorizationHeader 会将 'Authorization' header 设置为先前更新的令牌。如果您查看 catch 方法,您会发现它 return 是一个承诺(对于刷新令牌),最终将 return 另一个承诺(对于请求的实际第二次尝试) .
我已经尝试在不诉诸承诺的情况下做到这一点:
request(url: string, request: RequestOptionsArgs): Observable<Response> {
var me = this;
request.headers = request.headers || new Headers();
var isSecureCall: boolean = true; //url.toLowerCase().startsWith('https://');
if (isSecureCall === true) {
me.authService.setAuthorizationHeader(request.headers);
}
request.headers.append('Content-Type', 'application/json');
request.headers.append('Accept', 'application/json');
return this.http.request(url, request)
.catch(initialError => {
if (initialError && initialError.status === 401 && isSecureCall === true) {
// token might be expired, try to refresh token
return me.authService.refreshAuthenticationObservable().map((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request);
}
return Observable.throw(initialError);
});
}
else {
return Observable.throw(initialError);
}
});
}
上面的代码没有达到我的预期:在 200 响应的情况下,它正确地 returns 响应。但是,如果它捕获到 401,它将成功检索到新令牌,但订阅最终将检索到一个可观察对象而不是响应。我猜这是应该重试的未执行的 Observable。
我意识到将 promise 的工作方式转换为 rxjs 库可能不是最好的方法,但我还没有掌握 "everything is a stream" 的东西。我已经尝试了其他一些解决方案,包括 flatmap、retryWhen 等......但没有走多远,所以一些帮助表示赞赏。
通过快速查看您的代码,我会说您的问题似乎是您没有展平从 refresh
服务 return 编辑的 Observable
。
catch
运算符期望您 return 一个 Observable
它将连接到失败的 Observable 的末尾,以便下游 Observer
不会'不知道有什么区别。
在非 401 情况下,您通过 returning 一个重新抛出初始错误的 Observable 来正确地做到这一点。但是,在刷新情况下,您 returning 一个 Observable
会产生 更多 Observables
而不是单个值。
我建议您将刷新逻辑更改为:
return me.authService
.refreshAuthenticationObservable()
//Use flatMap instead of map
.flatMap((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request);
}
return Observable.throw(initialError);
});
flatMap
会将中间 Observables
转换为单个流。
在最新版本的 RxJs 中,flatMap
运算符已重命名为 mergeMap
。
我创建这个 demo 是为了了解如何使用 rxjs 处理刷新令牌。它这样做:
- 使用访问令牌进行 API 调用。
- 如果访问令牌过期(observable 抛出适当的错误),它会进行另一个异步调用以刷新令牌。
- 令牌刷新后,它将重试 API 调用。
- 如果还是报错,放弃。
此演示不进行实际的 HTTP 调用(它使用 Observable.create
模拟它们)。
相反,使用它来学习如何使用 catchError
和 retry
运算符来解决问题(访问令牌第一次失败),然后重试失败的操作(API 呼叫)。
自从我开始使用 angular2 以来,我已经将我的服务设置为 return T 的 Observable。在该服务中,我将调用 map(),而使用这些服务的组件将只使用 subscribe()等待响应。对于这些简单的场景,我真的不需要深入研究 rxjs,所以一切都很好。
我现在想要实现以下目标:我正在使用带有刷新令牌的 Oauth2 身份验证。我想构建一个所有其他服务都将使用的 api 服务,并且当 returned 出现 401 错误时,它将透明地处理刷新令牌。因此,对于 401,我首先从 OAuth2 端点获取一个新令牌,然后使用新令牌重试我的请求。下面是工作正常的代码,有承诺:
request(url: string, request: RequestOptionsArgs): Promise<Response> {
var me = this;
request.headers = request.headers || new Headers();
var isSecureCall: boolean = true; //url.toLowerCase().startsWith('https://');
if (isSecureCall === true) {
me.authService.setAuthorizationHeader(request.headers);
}
request.headers.append('Content-Type', 'application/json');
request.headers.append('Accept', 'application/json');
return this.http.request(url, request).toPromise()
.catch(initialError => {
if (initialError && initialError.status === 401 && isSecureCall === true) {
// token might be expired, try to refresh token.
return me.authService.refreshAuthentication().then((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request).toPromise();
}
return <any>Promise.reject(initialError);
});
}
else {
return <any>Promise.reject(initialError);
}
});
}
在上面的代码中,authService.refreshAuthentication() 将获取新令牌并将其存储在 localStorage 中。 authService.setAuthorizationHeader 会将 'Authorization' header 设置为先前更新的令牌。如果您查看 catch 方法,您会发现它 return 是一个承诺(对于刷新令牌),最终将 return 另一个承诺(对于请求的实际第二次尝试) .
我已经尝试在不诉诸承诺的情况下做到这一点:
request(url: string, request: RequestOptionsArgs): Observable<Response> {
var me = this;
request.headers = request.headers || new Headers();
var isSecureCall: boolean = true; //url.toLowerCase().startsWith('https://');
if (isSecureCall === true) {
me.authService.setAuthorizationHeader(request.headers);
}
request.headers.append('Content-Type', 'application/json');
request.headers.append('Accept', 'application/json');
return this.http.request(url, request)
.catch(initialError => {
if (initialError && initialError.status === 401 && isSecureCall === true) {
// token might be expired, try to refresh token
return me.authService.refreshAuthenticationObservable().map((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request);
}
return Observable.throw(initialError);
});
}
else {
return Observable.throw(initialError);
}
});
}
上面的代码没有达到我的预期:在 200 响应的情况下,它正确地 returns 响应。但是,如果它捕获到 401,它将成功检索到新令牌,但订阅最终将检索到一个可观察对象而不是响应。我猜这是应该重试的未执行的 Observable。
我意识到将 promise 的工作方式转换为 rxjs 库可能不是最好的方法,但我还没有掌握 "everything is a stream" 的东西。我已经尝试了其他一些解决方案,包括 flatmap、retryWhen 等......但没有走多远,所以一些帮助表示赞赏。
通过快速查看您的代码,我会说您的问题似乎是您没有展平从 refresh
服务 return 编辑的 Observable
。
catch
运算符期望您 return 一个 Observable
它将连接到失败的 Observable 的末尾,以便下游 Observer
不会'不知道有什么区别。
在非 401 情况下,您通过 returning 一个重新抛出初始错误的 Observable 来正确地做到这一点。但是,在刷新情况下,您 returning 一个 Observable
会产生 更多 Observables
而不是单个值。
我建议您将刷新逻辑更改为:
return me.authService
.refreshAuthenticationObservable()
//Use flatMap instead of map
.flatMap((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
// retry with new token
me.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request);
}
return Observable.throw(initialError);
});
flatMap
会将中间 Observables
转换为单个流。
在最新版本的 RxJs 中,flatMap
运算符已重命名为 mergeMap
。
我创建这个 demo 是为了了解如何使用 rxjs 处理刷新令牌。它这样做:
- 使用访问令牌进行 API 调用。
- 如果访问令牌过期(observable 抛出适当的错误),它会进行另一个异步调用以刷新令牌。
- 令牌刷新后,它将重试 API 调用。
- 如果还是报错,放弃。
此演示不进行实际的 HTTP 调用(它使用 Observable.create
模拟它们)。
相反,使用它来学习如何使用 catchError
和 retry
运算符来解决问题(访问令牌第一次失败),然后重试失败的操作(API 呼叫)。