Manejo de 401 a nivel mundial con Angular

90

En mi proyecto Angular 2, hago llamadas a API desde servicios que devuelven un Observable. El código de llamada luego se suscribe a este observable. Por ejemplo:

getCampaigns(): Observable<Campaign[]> {
    return this.http.get('/campaigns').map(res => res.json());
}

Digamos que el servidor devuelve un 401. ¿Cómo puedo detectar este error globalmente y redirigir a una página / componente de inicio de sesión?

Gracias.


Esto es lo que tengo hasta ahora:

// boot.ts

import {Http, XHRBackend, RequestOptions} from 'angular2/http';
import {CustomHttp} from './customhttp';

bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS,
    new Provider(Http, {
        useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
        deps: [XHRBackend, RequestOptions]
    })
]);

// customhttp.ts

import {Http, ConnectionBackend, Request, RequestOptions, RequestOptionsArgs, Response} from 'angular2/http';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class CustomHttp extends Http {
    constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
        super(backend, defaultOptions);
    }

    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {

        console.log('request...');

        return super.request(url, options);        
    }

    get(url: string, options?: RequestOptionsArgs): Observable<Response> {

        console.log('get...');

        return super.get(url, options);
    }
}

El mensaje de error que recibo es "backend.createConnection no es una función"

pbz
fuente
1
Creo que esto podría darte un pequeño
consejo

Respuestas:

78

Descripción

La mejor solución que he encontrado es anular el XHRBackendestado de respuesta HTTP 401y 403conduce a una acción en particular.

Si maneja su autenticación fuera de su aplicación Angular, podría forzar una actualización de la página actual de modo que se active su mecanismo externo. Detallo esta solución en la implementación a continuación.

También puede reenviar a un componente dentro de su aplicación de modo que su aplicación Angular no se recargue.

Implementación

Angular> 2.3.0

Gracias a @mrgoos, aquí hay una solución simplificada para angular 2.3.0+ debido a una corrección de errores en angular 2.3.0 (consulte el problema https://github.com/angular/angular/issues/11606 ) que extiende directamente el Httpmódulo.

import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';


@Injectable()
export class AuthenticatedHttpService extends Http {

  constructor(backend: XHRBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
  }
}

El archivo del módulo ahora solo contiene el siguiente proveedor.

providers: [
    { provide: Http, useClass: AuthenticatedHttpService }
]

Otra solución que utiliza Router y un servicio de autenticación externo se detalla en la siguiente esencia de @mrgoos.

Angular pre-2.3.0

La siguiente implementación funciona para Angular 2.2.x FINALy RxJS 5.0.0-beta.12.

Redirige a la página actual (más un parámetro para obtener una URL única y evitar el almacenamiento en caché) si se devuelve un código HTTP 401 o 403.

import { Request, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

export class AuthenticationConnectionBackend extends XHRBackend {

    constructor(_browserXhr: BrowserXhr, _baseResponseOptions: ResponseOptions, _xsrfStrategy: XSRFStrategy) {
        super(_browserXhr, _baseResponseOptions, _xsrfStrategy);
    }

    createConnection(request: Request) {
        let xhrConnection = super.createConnection(request);
        xhrConnection.response = xhrConnection.response.catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
        return xhrConnection;
    }

}

con el siguiente archivo de módulo.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule, XHRBackend } from '@angular/http';
import { AppComponent } from './app.component';
import { AuthenticationConnectionBackend } from './authenticated-connection.backend';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
    ],
    entryComponents: [AppComponent],
    imports: [
        BrowserModule,
        CommonModule,
        HttpModule,
    ],
    providers: [
        { provide: XHRBackend, useClass: AuthenticationConnectionBackend },
    ],
})
export class AppModule {
}
Nicolás Henneaux
fuente
2
¡Gracias! Descubrí mi problema ... Me faltaba esta línea, por eso catch()no se encontró. (smh) import "rxjs/add/operator/catch";
hartpdx
1
¿Es posible utilizar el módulo Router para hacer la navegación?
Yuanfei Zhu
1
¡Gran solución para combinar con Auth Guard! 1. Auth Guard verifica al usuario autorizado (por ejemplo, buscando en LocalStorage). 2. En la respuesta 401/403, limpia al usuario autorizado para el Guard (por ejemplo, eliminando los parámetros correspondientes en LocalStorage). 3. Como en esta etapa inicial no puede acceder al enrutador para reenviarlo a la página de inicio de sesión, la actualización de la misma página activará las comprobaciones de Guard, que lo remitirán a la pantalla de inicio de sesión (y opcionalmente conservará su URL inicial, por lo que será reenviado a la página solicitada después de una autenticación exitosa).
Alex Klaus
1
Hola @NicolasHenneaux, ¿por qué crees que es mejor anular http? El único beneficio que veo es que puede simplemente ponerlo como proveedor: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }mientras que al anular Http necesita escribir un código más incómodo useFactoryy limitarse llamando a 'nuevo' y enviando argumentos específicos. WDYT? Una referencia al segundo método: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett: he creado una esencia que debería ayudarlo: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
82

Angular 4.3+

Con la introducción de HttpClient vino la capacidad de interceptar fácilmente todas las solicitudes / respuestas. El uso general de HttpInterceptors está bien documentado , consulte el uso básico y cómo proporcionar el interceptor. A continuación se muestra un ejemplo de un HttpInterceptor que puede manejar errores 401.

Actualizado para RxJS 6+

import { Observable, throwError } from 'rxjs';
import { HttpErrorResponse, HttpEvent, HttpHandler,HttpInterceptor, HttpRequest } from '@angular/common/http';

import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status == 401) {
          // Handle 401 error
        } else {
          return throwError(err);
        }
      })
    );
  }

}

RxJS <6

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).do(event => {}, err => {
            if (err instanceof HttpErrorResponse && err.status == 401) {
                // handle 401 errors
            }
        });
    }
}
La daga de Gilbert Arenas
fuente
1
¿Esto sigue funcionando para ti? Ayer me funcionó, pero después de instalar otros módulos, aparece este error: next.handle (…) .do no es una función
Multitut
Creo que este debería usarse como extensión de clases como http es casi siempre un olor
kboom
1
No olvide agregarlo a su lista de proveedores con HTTP_INTERCEPTORS. Puede encontrar un ejemplo en los documentos
Bruno Peres
2
Genial, pero usar Routeraquí no parece funcionar. Por ejemplo, quiero dirigir a mis usuarios a la página de inicio de sesión cuando obtienen un 401-403, pero this.router.navigate(['/login']no me funciona. No hace nada
CodyBugstein
Si obtiene ".do no es una función", agréguelo import 'rxjs/add/operator/do';después de importar rxjs.
amoss
19

Como las API frontend caducan más rápido que la leche, con Angular 6+ y RxJS 5.5+, debe usar pipe:

import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 401) {
          this.router.navigate(['login'], { queryParams: { returnUrl: req.url } });
        }
        return throwError(err);
      })
    );
  }
}

Actualización para Angular 7+ y rxjs 6+

import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/internal/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((err, caught: Observable<HttpEvent<any>>) => {
          if (err instanceof HttpErrorResponse && err.status == 401) {
            this.router.navigate(['login'], { queryParams: { returnUrl: request.url } });
            return of(err as any);
          }
          throw err;
        })
      );
  }
}
Saeb Amini
fuente
Recibo error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.cuando el .pipeestá allí, no hay errores cuando .pipe
elimino
2
@BlackICE Supongo que eso reafirma la primera oración de mi respuesta. Actualicé con una respuesta para la versión más reciente.
Saeb Amini
1
En su ejemplo de ng7 + reqes en realidad request: la edición es
demasiado
12

El Observableque obtiene de cada método de solicitud es de tipo Observable<Response>. El Responseobjeto tiene una statuspropiedad que mantendrá el 401SI el servidor devolvió ese código. Por lo tanto, es posible que desee recuperarlo antes de asignarlo o convertirlo.

Si desea evitar hacer esta funcionalidad en cada llamada, es posible que tenga que extender la Httpclase de Angular 2 e inyectar su propia implementación que llama a parent ( super) para la Httpfuncionalidad regular y luego manejar el 401error antes de devolver el objeto.

Ver:

https://angular.io/docs/ts/latest/api/http/index/Response-class.html

Langley
fuente
Entonces, si extiendo Http, ¿debería poder redirigir a una ruta de "inicio de sesión" desde dentro de Http?
pbz
Ésa es la teoría. Tendrá que inyectar el enrutador en su implementación Http para hacerlo.
Langley
Gracias por tu ayuda. Actualicé la pregunta con un código de muestra. Probablemente estoy haciendo algo mal (siendo nuevo en Angular). ¿Alguna idea de que podría ser? Gracias.
pbz
Está utilizando los proveedores Http predeterminados, debe crear su propio proveedor que se resuelva en una instancia de su clase en lugar de la predeterminada. Ver: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@Langley, gracias. Tienes razón: subscribe ((result) => {}, (error) => {console.log (error.status);}. El parámetro de error sigue siendo de tipo Response.
abedurftig
9

Angular 4.3+

Para completar The Gilbert Arenas Dagger, responde:

Si lo que necesita es interceptar cualquier error, aplicarle un tratamiento y reenviarlo por la cadena (y no solo agregar un efecto secundario con .do), puede usar HttpClient y sus interceptores para hacer algo por el estilo:

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // install an error handler
        return next.handle(req).catch((err: HttpErrorResponse) => {
            console.log(err);
            if (err.error instanceof Error) {
                // A client-side or network error occurred. Handle it accordingly.
                console.log('An error occurred:', err.error.message);
            } else {
                // The backend returned an unsuccessful response code.
                // The response body may contain clues as to what went wrong,
                console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
            }

            return Observable.throw(new Error('Your custom error'));
        });
    }
}
Starscream
fuente
9

Para evitar el problema de la referencia cíclica que es causado por la inyección de servicios como "Router" en una clase derivada de Http, se debe utilizar el método Injector post-constructor. El siguiente código es una implementación funcional de un servicio Http que redirige a la ruta de inicio de sesión cada vez que una API REST devuelve "Token_Expired". Tenga en cuenta que se puede utilizar como una sustitución del Http normal y, como tal, no requiere cambiar nada en los componentes o servicios ya existentes de su aplicación.

app.module.ts

  providers: [  
    {provide: Http, useClass: ExtendedHttpService },
    AuthService,
    PartService,
    AuthGuard
  ],

extendido-http.service.ts

import { Injectable, Injector } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class ExtendedHttpService extends Http {
    private router; 
    private authService;

  constructor(  backend: XHRBackend, defaultOptions: RequestOptions, private injector: Injector) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
 
    if (typeof url === 'string') {
      if (!options) {
        options = { headers: new Headers() };
      }
      this.setHeaders(options);
    } else {
      this.setHeaders(url);
    }
    console.log("url: " + JSON.stringify(url) +", Options:" + options);

    return super.request(url, options).catch(this.catchErrors());
  }

  private catchErrors() {

    return (res: Response) => {
        if (this.router == null) {
            this.router = this.injector.get(Router);
        }
        if (res.status === 401 || res.status === 403) {
            //handle authorization errors
            //in this example I am navigating to login.
            console.log("Error_Token_Expired: redirecting to login.");
            this.router.navigate(['signin']);
        }
        return Observable.throw(res);
    };
  }

  private setHeaders(objectToSetHeadersTo: Request | RequestOptionsArgs) {
      
      if (this.authService == null) {
            this.authService = this.injector.get(AuthService);
      }
    //add whatever header that you need to every request
    //in this example I could set the header token by using authService that I've created
     //objectToSetHeadersTo.headers.set('token', this.authService.getToken());
  }
}

Tutmosis
fuente
8

Desde Angular> = 2.3.0 puede anular el HTTPmódulo e inyectar sus servicios. Antes de la versión 2.3.0, no podía usar sus servicios inyectados debido a un error central.

He creado una esencia para mostrar cómo se hace.

mrgoos
fuente
Gracias por armar eso. Recibía un error de compilación que decía "No se puede encontrar el nombre 'Http'" en app.module.ts, así que importé y ahora recibo el siguiente error: "¡No se puede crear una instancia de dependencia cíclica! Http: en NgModule AppModule"
Bryan
Hola @ Brett, ¿puedes compartir tu app.modulecódigo? Gracias.
mrgoos
Parece bien. ¿Puede agregar a la esencia el HTTP extendido? Además, ¿importa a HTTPalgún otro lugar?
mrgoos
Perdón por el retraso. Estoy en Angular 2.4 ahora y obtengo el mismo error. Importo Http en varios archivos. Aquí está mi esencia actualizada: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan
El mismo problema aquí ... Parece que esta esencia no funciona, así que tal vez deberíamos marcarlo como tal.
Tutmosis
2

Angular> 4.3: ErrorHandler para el servicio base

protected handleError(err: HttpErrorResponse | any) {
    console.log('Error global service');
    console.log(err);
    let errorMessage: string = '';

    if (err.hasOwnProperty('status')) { // if error has status
        if (environment.httpErrors.hasOwnProperty(err.status)) {
            // predefined errors
            errorMessage = environment.httpErrors[err.status].msg; 
        } else {
            errorMessage = `Error status: ${err.status}`;
            if (err.hasOwnProperty('message')) {
                errorMessage += err.message;
            }
        }
     }

    if (errorMessage === '') {
        if (err.hasOwnProperty('error') && err.error.hasOwnProperty('message')) { 
            // if error has status
            errorMessage = `Error: ${err.error.message}`;
        }
     }

    // no errors, then is connection error
    if (errorMessage === '') errorMessage = environment.httpErrors[0].msg; 

    // this.snackBar.open(errorMessage, 'Close', { duration: 5000 }});
    console.error(errorMessage);
    return Observable.throw(errorMessage);
}
gildniy
fuente