Obsługa 401 na całym świecie za pomocą Angular

92

W moim projekcie Angular 2 wykonuję wywołania API z usług, które zwracają Observable. Następnie kod wywołujący subskrybuje ten obserwowalny. Na przykład:

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

Powiedzmy, że serwer zwraca 401. Jak mogę wyłapać ten błąd globalnie i przekierować do strony logowania / komponentu?

Dzięki.


Oto, co mam do tej pory:

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

Komunikat o błędzie, który otrzymuję, to „backend.createConnection nie jest funkcją”

pbz
źródło
1
Myślę, że to może dać ci małą wskazówkę
Pankaj Parkar

Odpowiedzi:

79

Opis

Najlepszym rozwiązaniem, jakie znalazłem, jest zastąpienie XHRBackendtakiego stanu odpowiedzi HTTP 401i 403prowadzi do określonej akcji.

Jeśli obsługujesz uwierzytelnianie poza aplikacją Angular, możesz wymusić odświeżenie bieżącej strony, tak aby wyzwolony został mechanizm zewnętrzny. Wyszczególniam to rozwiązanie w realizacji poniżej.

Możesz również przekazać do składnika w aplikacji, tak aby aplikacja Angular nie została ponownie załadowana.

Realizacja

Kątowy> 2.3.0

Dzięki @mrgoos, tutaj jest uproszczone rozwiązanie dla angular 2.3.0+ z powodu poprawki błędu w angular 2.3.0 (patrz wydanie https://github.com/angular/angular/issues/11606 ) rozszerzające bezpośrednio Httpmoduł.

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

Plik modułu zawiera teraz tylko następującego dostawcę.

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

Innym rozwiązaniem przy użyciu routera i zewnętrzną usługę uwierzytelniania jest szczegółowo w poniższej GIST przez @mrgoos.

Angular przed 2.3.0

Następująca implementacja działa dla Angular 2.2.x FINALi RxJS 5.0.0-beta.12.

Przekierowuje do bieżącej strony (plus parametr umożliwiający uzyskanie unikalnego adresu URL i uniknięcie buforowania), jeśli zostanie zwrócony kod HTTP 401 lub 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;
    }

}

z następującym plikiem modułu.

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
źródło
2
Dzięki! Zrozumiałem mój problem ... Brakowało mi tej linii, dlatego catch()nie została znaleziona. (smh) import "rxjs/add/operator/catch";
hartpdx
1
Czy do nawigacji można wykorzystać moduł routera?
Yuanfei Zhu
1
Świetne rozwiązanie do łączenia z Auth Guard! 1. Auth Guard sprawdza autoryzowanego użytkownika (np. Zaglądając do LocalStorage). 2. W odpowiedzi 401/403 wyczyścisz autoryzowanego użytkownika Strażnika (np. Usuwając odpowiadające mu parametry w LocalStorage). 3. Ponieważ na tym wczesnym etapie nie możesz uzyskać dostępu do routera w celu przekierowania na stronę logowania, odświeżenie tej samej strony uruchomi kontrolę Guard, która przekieruje Cię do ekranu logowania (i opcjonalnie zachowa początkowy adres URL, więc „ zostanie przekierowany na żądaną stronę po pomyślnym uwierzytelnieniu).
Alex Klaus,
1
Hej @NicolasHenneaux - dlaczego uważasz, że lepiej to zastąpić http? Jedyną korzyścią, jaką widzę, jest to, że możesz po prostu podać go jako dostawcę: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }podczas gdy zastępując Http, musisz napisać bardziej niezręczny kod useFactoryi ograniczyć się, wywołując „nowy” i wysyłając określone argumenty. WDYT? Odniesienie do drugiej metody: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett - stworzyłem dla niego sedno, który powinien ci pomóc: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
84

Angular 4.3+

Wraz z wprowadzeniem HttpClient pojawiła się możliwość łatwego przechwytywania wszystkich żądań / odpowiedzi. Ogólne użycie HttpInterceptors jest dobrze udokumentowane , zobacz podstawowe użycie i sposób zapewnienia przechwytywacza. Poniżej znajduje się przykład HttpInterceptor, który może obsłużyć błędy 401.

Zaktualizowano dla 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
            }
        });
    }
}
Sztylet Gilberta Arenasa
źródło
1
Czy to nadal działa dla Ciebie? Wczoraj u mnie działało ale po zainstalowaniu innych modułów wyskakuje mi ten błąd: next.handle (…) .do nie jest funkcją
Multitut
Myślę, że ten powinien być używany jako rozszerzenie klas, takich jak http to prawie zawsze zapach
kboom
1
Nie zapomnij dodać go do swojej listy dostawców za pomocą HTTP_INTERCEPTORS. Możesz znaleźć przykład w dokumentach
Bruno Peres,
2
Świetnie, ale używanie Routertutaj wydaje się nie działać. Na przykład chcę skierować moich użytkowników do strony logowania, gdy otrzymają kod 401-403, ale this.router.navigate(['/login']dla mnie nie działa. To nic nie robi
CodyBugstein
Jeśli otrzymujesz „.do nie jest funkcją”, dodaj import 'rxjs/add/operator/do';po zaimportowaniu rxjs.
amoss
20

Ponieważ interfejsy API frontendu wygasają szybciej niż mleko, z Angular 6+ i RxJS 5.5+, musisz użyć 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);
      })
    );
  }
}

Aktualizacja dla Angular 7+ i 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
źródło
Dostaję error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.jak .pipetam jest brak błędów jak .pipe
wyjmę
2
@BlackICE Myślę, że to potwierdza pierwsze zdanie mojej odpowiedzi. Zaktualizowałem odpowiedź na najnowszą wersję.
Saeb Amini
1
W twoim przykładzie z ng7 + reqjest tak naprawdę request- edycja jest
zbyt
dlaczego używamy interceprot i dlaczego nie możemy sobie z tym poradzić przez wywołanie interfejsu API z metodą post
saber tabatabaee yazdi
12

Otrzymane Observablez każdej metody żądania jest typu Observable<Response>. ResponseObiekt, posiada statuswłaściwości, które odbędzie się 401, jeśli serwer wrócił ten kod. Więc możesz chcieć to odzyskać przed zmapowaniem lub przekonwertowaniem.

Jeśli chcesz uniknąć wykonywania tej funkcji przy każdym wywołaniu, być może będziesz musiał rozszerzyć Httpklasę Angular 2 i wstrzyknąć własną implementację, która wywołuje funkcję parent ( super) dla zwykłej Httpfunkcjonalności, a następnie obsłużyć 401błąd przed zwróceniem obiektu.

Widzieć:

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

Langley
źródło
Więc jeśli rozszerzę Http, powinienem być w stanie przekierować do trasy „logowania” z poziomu Http?
pbz
Taka jest teoria. Aby to zrobić, musisz wstrzyknąć router do implementacji HTTP.
Langley
Dzięki za pomoc. Zaktualizowałem pytanie przykładowym kodem. Prawdopodobnie robię coś nie tak (jestem nowy w Angular). Masz pojęcie, co to może być? Dzięki.
pbz
Używasz domyślnych dostawców HTTP, musisz utworzyć własnego dostawcę, który będzie rozpoznawany jako wystąpienie Twojej klasy zamiast domyślnego. Zobacz: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@Langley, dzięki. Masz rację: subscribe ((result) => {}, (error) => {console.log (error.status);}. Parametr błędu nadal jest typu Response.
abedurftig
9

Angular 4.3+

Aby ukończyć odpowiedź The Gilbert Arenas Dagger :

Jeśli potrzebujesz przechwycić jakikolwiek błąd, zastosuj do niego obróbkę i prześlij go dalej w dół łańcucha (a nie tylko dodaj efekt uboczny .do), możesz użyć HttpClient i jego przechwytywaczy, aby zrobić coś takiego:

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
źródło
9

Aby uniknąć problemu z cyklicznymi odwołaniami, który jest spowodowany wstawieniem usług takich jak „Router” do klasy pochodnej HTTP, należy użyć metody Injector po konstruktorze. Poniższy kod jest działającą implementacją usługi HTTP, która przekierowuje do trasy logowania za każdym razem, gdy interfejs API REST zwraca „Token_Expired”. Zauważ, że może być używany jako substytut dla zwykłego HTTP i jako taki nie wymaga zmiany niczego w już istniejących składnikach lub usługach aplikacji.

app.module.ts

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

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

Totmes
źródło
8

Od Angular> = 2.3.0 możesz nadpisać HTTPmoduł i wstrzyknąć swoje usługi. Przed wersją 2.3.0 nie można było używać wstrzykniętych usług z powodu podstawowego błędu.

Stworzyłem streszczenie, aby pokazać, jak to się robi.

mrgoos
źródło
Dzięki za złożenie tego w całość. Otrzymałem błąd kompilacji o treści „Nie można znaleźć nazwy 'Http'” w pliku app.module.ts, więc zaimportowałem i otrzymuję następujący błąd: „Nie można utworzyć instancji cyklicznej zależności! Http: w NgModule AppModule”
Bryan
Hej @ Brett - czy możesz udostępnić swój app.modulekod? Dzięki.
mrgoos
Wydaje się OK. Czy możesz dodać do sedna rozszerzony protokół HTTP? Ponadto czy importujesz HTTPgdzie indziej?
mrgoos
Przepraszam za opóźnienie. Jestem teraz na Angular 2.4 i otrzymuję ten sam błąd. Importuję Http w kilku plikach. Oto moja zaktualizowana treść: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan
Ten sam problem ... Wygląda na to, że to streszczenie nie działa, więc może powinniśmy to oznaczyć jako takie?
Totmes
2

Kątowy> 4,3: ErrorHandler dla usługi podstawowej

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
źródło