Manejo de tokens de actualización usando rxjs


Desde que empecé con angular2 he configurado mis servicios para devolver Observable of T. En el servicio tendría la llamada map (), y los componentes que usan estos servicios solo usarían subscribe() para esperar la respuesta. Para estos escenarios simples realmente no necesitaba profundizar en rxjs, así que todo estaba bien.

Ahora quiero lograr lo siguiente: estoy usando la autenticación Oauth2 con tokens de actualización. Quiero construir un servicio api que todos los demás servicios usarán, y que maneje de forma transparente el token de actualización cuando se devuelva un error 401. Por lo tanto, en el caso de un 401, primero obtengo un nuevo token desde el punto final OAuth2, y luego reintento mi solicitud con el nuevo token. A continuación se muestra el código que funciona bien, con promesas:

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

En el código anterior, AuthService.refreshAuthentication () buscará el nuevo token y lo almacenará en localStorage. AuthService.setAuthorizationHeader establecerá el encabezado 'Authorization' en token actualizado previamente. Si nos fijamos en la captura método, verá que devuelve una promesa (para el token de actualización) que, a su vez, eventualmente devolverá otra promesa (para el 2do intento real de la solicitud).

He intentado hacer esto sin recurrir a promesas:

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

El código anterior no hace lo que espero: en el caso de una respuesta de 200, devuelve correctamente la respuesta. Sin embargo, si atrapa el 401, recuperará con éxito el nuevo token, pero el suscriptor finalmente recuperará un observable en lugar de la respuesta. Supongo que este es el observable no ejecutado que debería hacer el reintento.

Me doy cuenta de que traducir la forma promise de trabajar en la biblioteca rxjs probablemente no sea la mejor manera de hacerlo, pero no he sido capaz de comprender lo de "todo es una corriente". He probado algunas otras soluciones que involucran flatmap, retryWhen, etc... pero no llegó muy lejos, así que un poco de ayuda es apreciada.

Author: Davy, 2016-01-20

3 answers

De un vistazo rápido a su código, diría que su problema parece ser que no está aplanando el Observable que se devuelve desde el servicio refresh.

El operador catch espera que devuelvas un Observable que se concatenará al final del observable fallido para que la corriente descendente Observer no conozca la diferencia.

En el caso no-401 está haciendo esto correctamente al devolver un Observable que replantea el error inicial. Sin embargo, en la actualización caso usted está devolviendo un {[1] } el produce más Observables en lugar de valores individuales.

Te sugiero que cambies la lógica de actualización para que sea:

    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 convertirá el intermedio Observables en un solo flujo.

 23
Author: paulpdaniels,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-21 07:18:15

En la última versión de RxJS, el operador flatMap ha sido renombrado a mergeMap.

 10
Author: mostefaiamine,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-05 12:57:19

He creado este demo para averiguar cómo manejar el token de actualización usando rxjs. Hace esto:

  • Realiza una llamada a la API con el token de acceso.
  • Si el token de acceso expiró (observable arroja el error apropiado), realiza otra llamada asincrónica para actualizar el token.
  • Una vez actualizado el token, volverá a intentar la llamada a la API.
  • Si aún hay error, ríndete.

Esta demo no hace llamadas HTTP reales (las simula usando Observable.create).

En su lugar, utilícelo para aprenda a usar los operadores catchError y retry para solucionar un problema (el token de acceso falló la primera vez) y, a continuación, vuelva a intentar la operación fallida (la llamada a la API).

 1
Author: kctang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-06 02:53:59