webentwicklung-frage-antwort-db.com.de

Umgang mit Refresh-Token mit rxjs

Seit ich mit angle2 angefangen habe, habe ich meine Dienste so eingerichtet, dass Observable of T zurückgegeben wird. Im Dienst hätte ich den map () - Aufruf, und Komponenten, die diese Dienste verwenden, würden einfach subscribe () verwenden, um auf die Antwort zu warten. Für diese einfachen Szenarien brauchte ich nicht wirklich zu rxjs, also war alles in Ordnung. 

Ich möchte nun folgendes erreichen: Ich verwende Oauth2-Authentifizierung mit Refresh-Token. Ich möchte einen API-Dienst erstellen, den alle anderen Dienste verwenden werden und der das Aktualisierungstoken transparent verarbeitet, wenn ein 401-Fehler zurückgegeben wird. Im Fall einer 401 rufe ich also zunächst ein neues Token vom OAuth2-Endpunkt ab und wiederhole dann meine Anforderung mit dem neuen Token. Der Code funktioniert gut und verspricht:

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

Im obigen Code holt authService.refreshAuthentication () das neue Token und speichert es in localStorage. authService.setAuthorizationHeader setzt den Header 'Authorization' auf das zuvor aktualisierte Token. Wenn Sie die catch-Methode betrachten, werden Sie sehen, dass sie ein Versprechen (für das Aktualisierungstoken) zurückgibt, das wiederum ein anderes Versprechen (für den zweiten Versuch der Anforderung) zurückgibt.

Ich habe versucht, dies zu tun, ohne auf Versprechen zurückzugreifen:

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

Der obige Code entspricht nicht dem, was ich erwarte: Im Fall einer 200-Antwort wird die Antwort ordnungsgemäß zurückgegeben. Wenn es jedoch den 401 abfängt, wird es erfolgreich das neue Token abrufen, aber der Abonnent wird schließlich ein Observable statt der Antwort abrufen. Ich denke, dies ist das nicht ausgeführte Observable, das die Wiederholung durchführen sollte.

Ich stelle fest, dass die Übersetzung der vielversprechenden Arbeitsweise in die rxjs-Bibliothek wahrscheinlich nicht der beste Weg ist, aber ich habe es bisher nicht geschafft, die "alles ist ein Strom" -Sache zu begreifen. Ich habe ein paar andere Lösungen ausprobiert, die Flatmap, RetryWhen etc ... beinhalten, aber nicht weit gekommen sind.

41
Davy

Nach einem kurzen Blick auf Ihren Code würde ich sagen, dass Ihr Problem scheinbar so ist, dass Sie nicht das Observable reduzieren, das vom refresh-Dienst zurückgegeben wird. 

Der catch-Operator erwartet, dass Sie ein Observable zurückgeben, das am Ende des fehlgeschlagenen Observable verkettet wird, sodass der Downstream Observer den Unterschied nicht kennt.

Im Nicht-401-Fall führen Sie dies korrekt aus, indem Sie ein Observable zurückgeben, das den ursprünglichen Fehler erneut auslöst. Im Aktualisierungsfall geben Sie jedoch ein Observable zurück, das more Observables anstelle von Einzelwerten erzeugt.

Ich würde vorschlagen, dass Sie die Aktualisierungslogik ändern:

    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 konvertiert das Zwischenprodukt Observables in einen einzelnen Stream.

23
paulpdaniels

In der neuesten Version von RxJs wurde der Operator flatMap in mergeMap umbenannt.

10
mostefaiamine

Ich habe dieses demo erstellt, um herauszufinden, wie das Refresh-Token mit rxjs gehandhabt werden kann. Es tut das:

  • Ruft einen API-Aufruf mit Zugriffstoken auf.
  • Wenn das Zugriffstoken abgelaufen ist (der beobachtbare Fehler löst einen entsprechenden Fehler aus), führt es einen weiteren asynchronen Aufruf aus, um das Token zu aktualisieren.
  • Sobald das Token aktualisiert wurde, wird der API-Aufruf erneut versucht.
  • Wenn immer noch Fehler, geben Sie auf.

Diese Demo führt keine tatsächlichen HTTP-Aufrufe aus (sie simuliert sie mit Observable.create).

Verwenden Sie es stattdessen, um zu erfahren, wie Sie mit den Operatoren catchError und retry ein Problem beheben (das Zugriffstoken ist beim ersten Mal fehlgeschlagen). Wiederholen Sie dann die fehlgeschlagene Operation (den API-Aufruf).

1
kctang