【问题标题】:Angular 4 http interceptor refresh_token triggered too many timesAngular 4 http拦截器refresh_token触发了太多次
【发布时间】:2017-07-03 16:39:42
【问题描述】:

我正在尝试在 Angular Http 中装饰内置的 request 方法。 我同时触发了多个链接和/或异步请求,这反过来导致 401 响应同时触发了很多次。由于多次使用相同的刷新令牌,这会使授权崩溃。

到目前为止,我已经走了这么远:

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch(error=> {
        if (error.status === 401) {
            return Observable.create((observer: Observer<Response>) => {
                // Use promise to avoid OPTIONS request
                this.oauthService.refreshToken().then((tokenResponse: Response) => {
                    console.debug('promise resolved');

                    observer.next(tokenResponse);
                    observer.complete();
                });
            }).mergeMap((tokenResponse) => {
                options = this.updateAuthHeader(options);
                return super.request(url, options);
            });
        } else {
            return Observable.throw(error);
        }
    });
}

我的问题似乎是我需要在遇到 401 时对待处理的请求进行查询,或者我只是缺乏如何使用 observables 来实现这一点的知识。据我了解,mergeMap 应该为我服务,但遗憾的是,它们都在我的令牌刷新之前触发了请求。

关于如何实现这一点的任何想法?

【问题讨论】:

  • 只是说,如果您的请求实际上是成功的(也就是不是 401),那么您将抛出一个错误;这可能不是理想的行为。快速修复:删除else
  • 嗯.. 是吗?它在 .catch() 中,女巫抛出错误,而不是成功的请求
  • 在这里查看我的回答,我提出了同样的问题:stackoverflow.com/questions/42390773/…
  • 所以基本上我需要扩展我的 oauthService.refreshToken() 使用可共享的 observable 而不是我目前使用的 return Observable.create(...)?
  • 我想我现在有一个解决方案。将在星期一发布。

标签: angular typescript oauth rxjs


【解决方案1】:

在@stely000、一些同事和其他在线社区的帮助下,我完成了这个:

由于我使用的解决方案是拦截来自 angular2/angular4 的内置 Http 服务,在此处找到 https://scotch.io/@kashyapmukkamala/using-http-interceptor-with-angular2,我的代码与我找到的其他解决方案略有不同。我在不同地方提到的OAuthService 可以在这里找到:https://github.com/manfredsteyer/angular-oauth2-oidc。由于该服务对Http 的依赖,我不得不在之后注入该服务以避免循环依赖。如果您对此有任何疑问,请尽管问我,我也会回答。 :)

基本上,如果后端服务以 401 响应,我的 refresh_token 功能触发一次的解决方案是通过三个步骤实现的:

  1. 创建一个共享的 observable 以避免 this.postRequest() 一次触发不止一次。

  2. 创建请求标头并将帖子添加到端点 refresh_token 被处理。

  3. 收听步骤 1 中的共享观察者。当令牌 已刷新,提取和更新本地存储中的数据。

现在,在我的构造函数中,我创建了共享的 observable:

constructor(
    backend: ConnectionBackend,
    defaultOptions: RequestOptions,
    private injector: Injector
) {
    super(backend, defaultOptions);
    // Step 1: Create an observable that is shared to avoid this.postRequest() to trigger more than once at a time
    this.refreshTokenObserver = Observable.defer(() => {
        return this.postRequest();
    }).share();
}

然后我创建一个方法来发布刷新access_token 的请求(注意:这基本上是OAuthService 中保存的代码的副本。这是因为该方法不是公开的服务):

// This method will only be triggered once at a time thanks to she shared observer above (Step 1).
private postRequest(): Observable<any> {
    // Step 2: Create request headers and add post to endpoint where refresh_token is handled.
    let search = new URLSearchParams();
    search.set('grant_type', 'refresh_token');
    search.set('client_id', this.oauthService.clientId);
    search.set('scope', '');
    search.set('refresh_token', localStorage.getItem('refresh_token'));

    let headers = new Headers();
    headers.set('Content-Type', 'application/x-www-form-urlencoded');

    let params = search.toString();

    return super.post(this.oauthService.tokenEndpoint, params, { headers }).map(r => r.json());
}

然后我有一种方法可以提取数据并使用来自端点的响应更新本地存储:

// This method is triggered when the server responds with 401 due to expired access_token or other reasons
private refreshToken() {
    // Step 3: Listen to the shared observer from step 1. When the token has been refreshed, extract and update data in localstorage
    return this.refreshTokenObserver.do((tokenResponse) => {
        localStorage.setItem("access_token", tokenResponse.access_token);
        if (tokenResponse.expires_in) {
            var expiresInMilliSeconds = tokenResponse.expires_in * 1000;
            var now = new Date();
            var expiresAt = now.getTime() + expiresInMilliSeconds;
            localStorage.setItem("expires_at", "" + expiresAt);
        }

        if (tokenResponse.refresh_token) {
            localStorage.setItem("refresh_token", tokenResponse.refresh_token);
        }
    },
    (err) => {
        console.error('Error performing password flow', err);
        return Observable.throw(err);
    });
}

为了启动上述步骤,需要触发初始请求并回复401

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch(error=> {
        if (error.status === 401) {
            // It the token has been expired trigger a refresh and after that continue the original request again with updated authorization headers.
            return this.refreshToken().mergeMap(() => {
                options = this.updateAuthHeader(options);
                return super.request(url, options);
            });
        } else {
            return Observable.throw(error);
        }
    });
}

奖励:我用于更新授权标头的方法基本上是使用上面提到的OAuthService 中的功能:

private updateAuthHeader(options: RequestOptionsArgs) {
    options.headers.set('Authorization', this.oauthService.authorizationHeader());

    return options;
}

反思/想法: 我最初的想法是使用OAuthService 来刷新令牌。由于 promises 和 observables 的混合,这比我预期的要难。我可能可以更改postRequest 方法以使用上述服务方法。我真的不知道更好/更清洁的解决方案可能是什么。

另外,我认为每个人都应该可以找到一个简单的解决方案。我自己很难做到这一点,我感谢所有帮助过我的人(包括 SO、IRL 和其他社区)。

【讨论】:

    猜你喜欢
    • 2017-11-07
    • 2018-05-19
    • 1970-01-01
    • 2018-07-08
    • 1970-01-01
    • 2023-03-13
    • 1970-01-01
    • 2018-03-30
    • 2018-05-17
    相关资源
    最近更新 更多