使用 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 模拟它们)。

相反,使用它来学习如何使用 catchErrorretry 运算符来解决问题(访问令牌第一次失败),然后重试失败的操作(API 呼叫)。