Gestire i 401 a livello globale con Angular


90

Nel mio progetto Angular 2 effettuo chiamate API da servizi che restituiscono un Observable. Il codice chiamante quindi sottoscrive questo osservabile. Per esempio:

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

Supponiamo che il server restituisca un 401. Come posso rilevare questo errore a livello globale e reindirizzare a una pagina / componente di accesso?

Grazie.


Ecco cosa ho finora:

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

Il messaggio di errore che ricevo è "backend.createConnection non è una funzione"


1
Penso che questo potrebbe darti un piccolo suggerimento
Pankaj Parkar

Risposte:


78

Descrizione

La soluzione migliore che ho trovato è sovrascrivere XHRBackendtale che lo stato della risposta HTTP 401e 403porti a una particolare azione.

Se gestisci la tua autenticazione al di fuori della tua applicazione Angular, potresti forzare un aggiornamento della pagina corrente in modo che il tuo meccanismo esterno venga attivato. Descrivo in dettaglio questa soluzione nell'implementazione di seguito.

Puoi anche inoltrare a un componente all'interno della tua applicazione in modo che la tua applicazione Angular non venga ricaricata.

Implementazione

Angolare> 2.3.0

Grazie a @mrgoos, ecco una soluzione semplificata per angular 2.3.0+ a causa di una correzione di bug in angular 2.3.0 (vedi problema https://github.com/angular/angular/issues/11606 ) che estende direttamente il Httpmodulo.

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

Il file del modulo ora contiene solo il seguente provider.

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

Un'altra soluzione utilizzando router e un servizio di autenticazione esterna è dettagliata nella seguente Gist da @mrgoos.

Angolare pre-2.3.0

La seguente implementazione funziona per Angular 2.2.x FINALe RxJS 5.0.0-beta.12.

Reindirizza alla pagina corrente (più un parametro per ottenere un URL univoco ed evitare la memorizzazione nella cache) se viene restituito un codice 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 il seguente file di modulo.

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 {
}

2
Grazie! Ho capito il mio problema ... mi mancava questa riga, motivo per cui catch()non è stata trovata. (smh) import "rxjs/add/operator/catch";
hartpdx

1
È possibile utilizzare il modulo Router per fare la navigazione?
Yuanfei Zhu,

1
Ottima soluzione per il raggruppamento con Auth Guard! 1. Auth Guard controlla l'utente autorizzato (ad esempio esaminando LocalStorage). 2. Alla risposta 401/403 si pulisce l'utente autorizzato per il Guard (ad es. Rimuovendo i parametri corrispondenti in LocalStorage). 3. Poiché in questa fase iniziale non è possibile accedere al router per l'inoltro alla pagina di accesso, l'aggiornamento della stessa pagina attiverà i controlli di Guard, che ti inoltreranno alla schermata di accesso (e, facoltativamente, manterrai l'URL iniziale, quindi " verrà inoltrato alla pagina richiesta dopo l'autenticazione riuscita).
Alex Klaus

1
Ehi @NicolasHenneaux, perché pensi che sia meglio quindi ignorare http? L'unico vantaggio che vedo è che puoi semplicemente metterlo come provider: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }mentre sovrascrivendo Http devi scrivere codice più scomodo come useFactorye limitarti chiamando "nuovo" e inviando argomenti specifici. WDYT? Un riferimento al 2 ° metodo: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos

3
@Brett - Ho creato una sintesi per questo che dovrebbe aiutarti: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos

82

Angular 4.3+

Con l'introduzione di HttpClient è arrivata la possibilità di intercettare facilmente tutte le richieste / risposte. L'utilizzo generale di HttpInterceptors è ben documentato , vedere l'utilizzo di base e come fornire l'interceptor. Di seguito è riportato un esempio di un HttpInterceptor in grado di gestire errori 401.

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

1
Funziona ancora per te? Ieri per me funzionava, ma dopo aver installato altri moduli, ricevo questo errore: next.handle (…) .do non è una funzione
Multitut

Penso che questo dovrebbe essere usato come estensione di classi come http è quasi sempre un odore
kboom

1
Non dimenticare di aggiungerlo all'elenco dei provider con HTTP_INTERCEPTORS. Puoi trovare un esempio nei documenti
Bruno Peres

2
Ottimo ma l'utilizzo Routerqui non sembra funzionare. Ad esempio, desidero indirizzare i miei utenti alla pagina di accesso quando ottengono un 401-403, ma this.router.navigate(['/login']non funziona per me. Non fa nulla
CodyBugstein

Se ottieni ".do non è una funzione", aggiungi import 'rxjs/add/operator/do';dopo aver importato rxjs.
amoss

19

Poiché le API frontend scadono più velocemente del latte, con Angular 6+ e RxJS 5.5+, devi utilizzare 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);
      })
    );
  }
}

Aggiornamento per Angular 7+ e 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;
        })
      );
  }
}

Ottengo error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.quando .pipeè presente, nessun errore quando rimuovo il.pipe
BlackICE

2
@ BlackICE Immagino che ribadisca la prima frase della mia risposta. Ho aggiornato con una risposta per la versione più recente.
Saeb Amini

1
Nel tuo esempio ng7 + reqè in realtà request- la modifica è
troppo

12

Quello Observableche ottieni da ogni metodo di richiesta è di tipo Observable<Response>. L' Responseoggetto ha una statusproprietà che manterrà l' 401IF il server ha restituito quel codice. Quindi potresti volerlo recuperare prima di mapparlo o convertirlo.

Se vuoi evitare di fare questa funzionalità su ogni chiamata potresti dover estendere la Httpclasse di Angular 2 e iniettare la tua implementazione che chiama parent ( super) per la Httpfunzionalità normale e quindi gestire l' 401errore prima di restituire l'oggetto.

Vedere:

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


Quindi, se estendo Http, dovrei essere in grado di reindirizzare a un percorso di "accesso" dall'interno di Http?
pbz

Questa è la teoria. Dovrai iniettare il router nella tua implementazione Http per farlo.
Langley

Grazie per l'aiuto. Ho aggiornato la domanda con un codice di esempio. Probabilmente sto facendo qualcosa di sbagliato (essendo nuovo in Angular). Qualche idea di cosa potrebbe essere? Grazie.
pbz

Stai usando i provider Http predefiniti, devi creare il tuo provider che si risolve in un'istanza della tua classe invece di quella predefinita. Vedi: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley

1
@ Langley, grazie. Hai ragione: subscribe ((result) => {}, (error) => {console.log (error.status);}. Il parametro di errore è ancora di tipo Response.
abedurftig

9

Angular 4.3+

Per completare la risposta di The Gilbert Arenas Dagger :

Se ciò di cui hai bisogno è intercettare qualsiasi errore, applicare un trattamento ad esso e inoltrarlo lungo la catena (e non solo aggiungere un effetto collaterale con .do), puoi usare HttpClient ei suoi intercettori per fare qualcosa del genere:

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

9

Per evitare il problema di riferimento ciclico causato dall'iniezione di servizi come "Router" in una classe derivata Http, è necessario utilizzare il metodo Injector post-costruttore. Il codice seguente è un'implementazione funzionante di un servizio Http che reindirizza alla route di accesso ogni volta che un'API REST restituisce "Token_Expired". Si noti che può essere utilizzato in sostituzione del normale Http e, come tale, non richiede di modificare nulla nei componenti o servizi già esistenti dell'applicazione.

app.module.ts

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

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


8

Da Angular> = 2.3.0 puoi sovrascrivere il HTTPmodulo e iniettare i tuoi servizi. Prima della versione 2.3.0, non era possibile utilizzare i servizi iniettati a causa di un bug principale.

Ho creato una sintesi per mostrare come è fatto.


Grazie per averlo messo insieme. Ho ricevuto un errore di compilazione che diceva "Impossibile trovare il nome 'Http'" in app.module.ts, quindi ho importato e ora ricevo il seguente errore: "Impossibile creare un'istanza della dipendenza ciclica! Http: in NgModule AppModule"
Bryan

Hey @ Brett, puoi condividere il tuo app.modulecodice? Grazie.
mrgoos

Sembra a posto. Puoi aggiungere all'essenza l'HTTP esteso? Inoltre, HTTPimporti altrove?
mrgoos

Scusa per il ritardo. Ora sono su Angular 2.4 e ricevo lo stesso errore. Importo Http in diversi file. Ecco la mia sintesi
Bryan

Lo stesso problema qui ... Sembra che questa sintesi non funzioni, quindi forse dovremmo contrassegnarla come tale?
Tuthmosis

2

Angular> 4.3: ErrorHandler per il servizio di 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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.