web-dev-qa-db-fra.com

Gestion des jetons d'actualisation à l'aide de rxjs

Depuis que j'ai commencé avec angular2, j’ai configuré mes services pour renvoyer Observable of T. Dans le service, j’aurais l’appel map () et les composants utilisant ces services utiliseraient simplement subscribe () pour attendre la réponse. Pour ces scénarios simples, je n'avais pas vraiment besoin de creuser dans rxjs, donc tout allait bien. 

Je souhaite maintenant atteindre les objectifs suivants: J'utilise l'authentification Oauth2 avec des jetons d'actualisation. Je souhaite créer un service api que tous les autres services utiliseront, et qui gérera de manière transparente le jeton d'actualisation lorsqu'une erreur 401 sera renvoyée. Ainsi, dans le cas d'un 401, je récupère d'abord un nouveau jeton à partir du noeud final OAuth2, puis réessaie ma demande avec le nouveau jeton. Voici le code qui fonctionne bien, avec des promesses:

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

Dans le code ci-dessus, authService.refreshAuthentication () récupérera le nouveau jeton et le stockera dans localStorage. authService.setAuthorizationHeader définira l'en-tête 'Authorization' sur le jeton précédemment mis à jour. Si vous examinez la méthode catch, vous verrez qu'elle renvoie une promesse (pour le jeton d'actualisation) qui, à son tour, renverra une autre promesse (pour le deuxième essai réel de la demande).

J'ai essayé de le faire sans recourir à des promesses:

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

Le code ci-dessus ne fait pas ce que j'attendais: dans le cas d'une réponse 200, il renvoie correctement la réponse. Cependant, s'il attrape le 401, il récupérera avec succès le nouveau jeton, mais l'abonné souscrira finalement à un observable au lieu de la réponse. Je suppose que c’est l’observable non exécuté qui devrait faire l’essai à nouveau.

Je me rends compte que traduire la manière la plus prometteuse de travailler dans la bibliothèque de rxjs n’est probablement pas la meilleure solution, mais je n’ai pas été en mesure de saisir le principe "tout est flot". J'ai essayé quelques autres solutions impliquant flatmap, retryWhen etc ... mais je ne suis pas allé loin, donc une aide est appréciée.

41
Davy

Après un coup d’œil rapide à votre code, je dirais que votre problème semble être que vous n’aplatissez pas la Observable renvoyée par le service refresh

L'opérateur catch s'attend à ce que vous retourniez une Observable qu'il concaténera à la fin de l'observable en échec, de sorte que le flux aval Observer ne connaisse pas la différence.

Dans le cas contraire à 401, vous effectuez cette opération correctement en renvoyant un observable qui renvoie l'erreur initiale. Cependant, dans le cas d'actualisation, vous renvoyez une Observable, qui produit plus Observables au lieu de valeurs uniques.

Je vous suggère de changer la logique de rafraîchissement pour être:

    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 convertira la Observables intermédiaire en un seul flux.

23
paulpdaniels

Dans la dernière version de RxJs, l'opérateur flatMap a été renommé en mergeMap.

10
mostefaiamine

J'ai créé cette demo pour comprendre comment gérer le jeton d'actualisation à l'aide de rxjs. Il fait ceci:

  • Effectue un appel d'API avec un jeton d'accès.
  • Si le jeton d'accès a expiré (l'observable génère une erreur appropriée), il effectue un autre appel asynchrone pour actualiser le jeton.
  • Une fois le jeton actualisé, il réessayera l'appel de l'API.
  • Si l'erreur persiste, abandonnez.

Cette démo ne fait pas d'appels HTTP réels (elle les simule à l'aide de Observable.create).

Utilisez-le plutôt pour apprendre à utiliser les opérateurs catchError et retry pour résoudre un problème (le jeton d'accès a échoué pour la première fois), puis réessayez l'opération ayant échoué (l'appel d'API).

1
kctang