Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
540 views
in Technique[技术] by (71.8m points)

angular - Handling refresh tokens using rxjs

Since i've started with angular2 i have setup my services to return Observable of T. In the service i would have the map() call, and components using these services would just use subscribe() to wait for the response. For these simple scenarios i didnt really need to dig in to rxjs so all was ok.

I now want to achieve the following: i am using Oauth2 authentication with refresh tokens. I want to build an api service that all other services will use, and that will transparently handle the refresh token when a 401 error is returned. So, in the case of a 401, i first fetch a new token from the OAuth2 endpoint, and then retry my request with the new token. Below is the code that works fine, with promises:

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);
            }
        });
}

In the code above, authService.refreshAuthentication() will fetch the new token and store it in localStorage. authService.setAuthorizationHeader will set the 'Authorization' header to previously updated token. If you look at the catch method, you'll see that it returns a promise (for the refresh token) that in its turns will eventually return another promise (for the actual 2nd try of the request).

I have attempted to do this without resorting to promises:

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);
            }
        });
}

The code above does not do what i expect: in the case of a 200 response, it properly returns the response. However, if it catches the 401, it will successfully retrieve the new token, but the subscribe wil eventually retrieve an observable instead of the response. Im guessing this is the unexecuted Observable that should do the retry.

I realize that translating the promise way of working onto the rxjs library is probably not the best way to go, but i havent been able to grasp the "everything is a stream" thing. I have tried a few other solutions involving flatmap, retryWhen etc ... but didnt get far, so some help is appreciated.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

From a quick look at your code I would say that your problem seems to be that you are not flattening the Observable that is returned from the refresh service.

The catch operator expects that you will return an Observable that it will concatenate onto the end of the failed Observable so that the down stream Observer doesn't know the difference.

In the non-401 case you are doing this correctly by returning an Observable that rethrows the initial error. However in the refresh case you are returning an Observable the produces more Observables instead of single values.

I would suggest you change the refresh logic to be:

    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 will convert the intermediate Observables into a single stream.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...