Weltweiter Umgang mit 401s mit Angular

90

In meinem Angular 2-Projekt mache ich API-Aufrufe von Diensten, die eine Observable zurückgeben. Der aufrufende Code abonniert dann dieses Observable. Beispielsweise:

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

Angenommen, der Server gibt eine 401 zurück. Wie kann ich diesen Fehler global abfangen und zu einer Anmeldeseite / Komponente umleiten?

Vielen Dank.


Folgendes habe ich bisher:

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

Die Fehlermeldung lautet "backend.createConnection ist keine Funktion".

pbz
quelle
1
Ich denke, dies könnte Ihnen einen kleinen
Hinweis

Antworten:

78

Beschreibung

Die beste Lösung , die ich gefunden habe , ist das außer Kraft zu setzen , XHRBackendso dass der HTTP - Antwort - Status 401und 403führt zu einer bestimmten Aktion.

Wenn Sie Ihre Authentifizierung außerhalb Ihrer Angular-Anwendung durchführen, können Sie eine Aktualisierung der aktuellen Seite erzwingen, sodass Ihr externer Mechanismus ausgelöst wird. Ich beschreibe diese Lösung in der folgenden Implementierung.

Sie können auch an eine Komponente in Ihrer Anwendung weiterleiten, sodass Ihre Angular-Anwendung nicht neu geladen wird.

Implementierung

Winkel> 2.3.0

Dank @mrgoos ist hier eine vereinfachte Lösung für Angular 2.3.0+ aufgrund einer Fehlerbehebung in Angular 2.3.0 (siehe Problem https://github.com/angular/angular/issues/11606 ), die das HttpModul direkt erweitert .

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

Die Moduldatei enthält nur noch den folgenden Anbieter.

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

Eine weitere Lösung, die einen Router und einen externen Authentifizierungsdienst verwendet, wird in der folgenden Übersicht von @mrgoos beschrieben.

Winkel vor 2.3.0

Die folgende Implementierung funktioniert für Angular 2.2.x FINALund RxJS 5.0.0-beta.12.

Es wird zur aktuellen Seite umgeleitet (plus einem Parameter, um eine eindeutige URL zu erhalten und das Caching zu vermeiden), wenn ein HTTP-Code 401 oder 403 zurückgegeben wird.

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

}

mit der folgenden Moduldatei.

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 {
}
Nicolas Henneaux
quelle
2
Vielen Dank! Ich habe mein Problem herausgefunden ... Mir fehlte diese Zeile, weshalb sie catch()nicht gefunden wurde. (smh) import "rxjs/add/operator/catch";
hartpdx
1
Ist es möglich, das Routermodul für die Navigation zu verwenden?
Yuanfei Zhu
1
Tolle Lösung für die Bündelung mit Auth Guard! 1. Auth Guard überprüft den autorisierten Benutzer (z. B. durch einen Blick auf LocalStorage). 2. Bei der Antwort 401/403 bereinigen Sie den autorisierten Benutzer für den Guard (z. B. indem Sie die entsprechenden Parameter in LocalStorage entfernen). 3. Da Sie zu diesem frühen Zeitpunkt nicht auf den Router zugreifen können, um zur Anmeldeseite weiterzuleiten, werden beim Aktualisieren derselben Seite die Guard-Überprüfungen ausgelöst, die Sie zum Anmeldebildschirm weiterleiten (und optional Ihre ursprüngliche URL beibehalten, sodass Sie wird nach erfolgreicher Authentifizierung an die angeforderte Seite weitergeleitet).
Alex Klaus
1
Hey @NicolasHenneaux - warum denkst du, ist es besser als zu überschreiben http? Der einzige Vorteil, den ich sehe, ist, dass Sie es einfach als Anbieter angeben können: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }Wenn Sie HTTP überschreiben, müssen Sie umständlicheren Code schreiben useFactoryund sich einschränken, indem Sie "neu" aufrufen und bestimmte Argumente senden. WDYT? Ein Verweis auf die 2. Methode: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett - Ich habe eine Übersicht dafür erstellt, die Ihnen helfen soll: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
82

Winkel 4.3+

Mit der Einführung von HttpClient wurde die Möglichkeit geschaffen, alle Anfragen / Antworten einfach abzufangen. Die allgemeine Verwendung von HttpInterceptors ist gut dokumentiert. Informationen zur grundlegenden Verwendung und zur Bereitstellung des Interceptors finden Sie hier. Unten finden Sie ein Beispiel für einen HttpInterceptor, der 401-Fehler verarbeiten kann.

Aktualisiert für 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
            }
        });
    }
}
Der Gilbert Arenas Dolch
quelle
1
Funktioniert das noch für dich? Gestern hat es bei mir funktioniert, aber nach der Installation anderer Module wird folgende Fehlermeldung angezeigt: next.handle (…) .do ist keine Funktion
Multitut
Ich denke, dieses sollte als Erweiterung von Klassen verwendet werden, wie http ist fast immer ein Geruch
kboom
1
Vergessen Sie nicht, es mit HTTP_INTERCEPTORS zu Ihrer Anbieterliste hinzuzufügen. Sie finden ein Beispiel in den Dokumenten
Bruno Peres
2
Großartig, aber Routerhier zu verwenden scheint nicht zu funktionieren. Zum Beispiel möchte ich meine Benutzer zur Anmeldeseite weiterleiten, wenn sie einen 401-403 erhalten, this.router.navigate(['/login']funktioniert aber nicht für mich. Es macht nichts
CodyBugstein
Wenn Sie ".do ist keine Funktion" erhalten, fügen import 'rxjs/add/operator/do';Sie nach dem Importieren von rxjs hinzu.
Amoss
18

Da Frontend-APIs mit Angular 6+ und RxJS 5.5+ schneller ablaufen als Milch, müssen Sie Folgendes verwenden 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);
      })
    );
  }
}

Update für Angular 7+ und 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
quelle
Ich bekomme, error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.wenn das .pipe.pipe
drin
2
@BlackICE Ich denke, das bestätigt den ersten Satz in meiner Antwort. Ich habe mit einer Antwort für die neueste Version aktualisiert.
Saeb Amini
1
In Ihrem ng7 + Beispiel reqist tatsächlich request- die Bearbeitung ist zu klein für mich
ask_io
12

Die, die ObservableSie von jeder Anforderungsmethode erhalten, ist vom Typ Observable<Response>. Das ResponseObjekt hat eine statusEigenschaft, die das enthält401 IF enthält, wenn der Server diesen Code zurückgegeben hat. Vielleicht möchten Sie das abrufen, bevor Sie es zuordnen oder konvertieren.

Wenn Sie diese Funktionalität bei jedem Aufruf vermeiden möchten, müssen Sie möglicherweise die HttpKlasse von Angular 2 erweitern und eine eigene Implementierung einfügen, die parent ( super) für die reguläre HttpFunktionalität aufruft, und dann den 401Fehler behandeln, bevor Sie das Objekt zurückgeben.

Sehen:

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

Langley
quelle
Wenn ich also HTTP erweitere, sollte ich in der Lage sein, aus dem HTTP auf eine "Anmelderoute" umzuleiten?
pbz
Das ist die Theorie. Sie müssen den Router in Ihre HTTP-Implementierung einfügen, um dies zu tun.
Langley
Danke für Ihre Hilfe. Ich habe die Frage mit einem Beispielcode aktualisiert. Ich mache wahrscheinlich etwas falsch (neu bei Angular). Irgendeine Idee, was es sein könnte? Vielen Dank.
pbz
Wenn Sie die Standard-HTTP-Anbieter verwenden, müssen Sie einen eigenen Anbieter erstellen, der anstelle des Standardanbieters in eine Instanz Ihrer Klasse aufgelöst wird. Siehe: angle.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@ Langley, danke. Sie haben Recht: subscribe ((result) => {}, (error) => {console.log (error.status);}. Der Fehlerparameter ist immer noch vom Typ Response.
abedurftig
9

Winkel 4.3+

Um den Gilbert Arenas Dolch zu vervollständigen Antwort von :

Wenn Sie einen Fehler abfangen müssen, eine Behandlung darauf anwenden und ihn entlang der Kette weiterleiten (und nicht nur einen Nebeneffekt hinzufügen .do), können Sie HttpClient und seine Abfangjäger verwenden, um etwas in der Art zu tun:

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

Um das Problem der zyklischen Referenzierung zu vermeiden, das durch das Injizieren von Diensten wie "Router" in eine von HTTP abgeleitete Klasse verursacht wird, muss die Post-Konstruktor-Injector-Methode verwendet werden. Der folgende Code ist eine funktionierende Implementierung eines HTTP-Dienstes, der jedes Mal, wenn eine REST-API "Token_Expired" zurückgibt, zur Anmelderoute umleitet. Beachten Sie, dass es als Ersatz für das reguläre HTTP verwendet werden kann und daher keine Änderungen an den bereits vorhandenen Komponenten oder Diensten Ihrer Anwendung erforderlich macht.

app.module.ts

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

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

Tuthmosis
quelle
8

Ab Angular> = 2.3.0 können Sie das HTTPModul überschreiben und Ihre Dienste einspeisen. Vor Version 2.3.0 konnten Sie Ihre injizierten Dienste aufgrund eines Kernfehlers nicht verwenden.

Ich habe einen Kern erstellt, um zu zeigen, wie es gemacht wird.

mrgoos
quelle
Danke, dass du das zusammengestellt hast. Ich habe einen Build-Fehler mit der Meldung "Name 'Http' kann nicht gefunden werden" in app.module.ts erhalten, daher habe ich importiert und erhalte jetzt den folgenden Fehler: "Zyklische Abhängigkeit kann nicht instanziiert werden! Http: in NgModule AppModule"
Bryan
Hey @ Brett- kannst du deinen app.moduleCode teilen ? Vielen Dank.
Mrgoos
Es scheint in Ordnung zu sein. Können Sie das erweiterte HTTP zum Kern hinzufügen? Importieren Sie außerdem HTTPwoanders?
Mrgoos
Entschuldigung für die Verspätung. Ich bin jetzt auf Angular 2.4 und erhalte den gleichen Fehler. Ich importiere HTTP in mehrere Dateien. Hier ist meine aktualisierte Liste: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan
Gleiches Problem hier ... Sieht so aus, als ob dieses Wesentliche nicht funktioniert. Vielleicht sollten wir es als solches markieren?
Tuthmosis
2

Winkel> 4.3: ErrorHandler für den Basisdienst

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
quelle