Gestion des 401 dans le monde avec Angular

90

Dans mon projet Angular 2, je fais des appels API à partir de services qui renvoient un Observable. L'indicatif d'appel souscrit alors à cet observable. Par exemple:

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

Disons que le serveur renvoie un 401. Comment puis-je intercepter globalement cette erreur et rediriger vers une page / un composant de connexion?

Merci.


Voici ce que j'ai jusqu'à présent:

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

Le message d'erreur que j'obtiens est "backend.createConnection n'est pas une fonction"

pbz
la source
1
Je pense que cela pourrait vous donner un petit indice
Pankaj Parkar

Réponses:

78

La description

La meilleure solution que j'ai trouvée est de remplacer le XHRBackendtel que l'état de la réponse HTTP 401et 403conduit à une action particulière.

Si vous gérez votre authentification en dehors de votre application Angular, vous pouvez forcer une actualisation de la page actuelle de sorte que votre mécanisme externe se déclenche. Je détaille cette solution dans l'implémentation ci-dessous.

Vous pouvez également transférer vers un composant de votre application afin que votre application Angular ne soit pas rechargée.

la mise en oeuvre

Angulaire> 2,3,0

Grâce à @mrgoos, voici une solution simplifiée pour angular 2.3.0+ en raison d'un bug corrigé dans angular 2.3.0 (voir issue https://github.com/angular/angular/issues/11606 ) étendant directement le Httpmodule.

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

Le fichier de module contient désormais uniquement le fournisseur suivant.

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

Une autre solution utilisant Router et un service d'authentification externe est détaillée dans l' essentiel suivant par @mrgoos.

Angulaire pré-2.3.0

L'implémentation suivante fonctionne pour Angular 2.2.x FINALet RxJS 5.0.0-beta.12.

Il redirige vers la page actuelle (plus un paramètre pour obtenir une URL unique et éviter la mise en cache) si un code HTTP 401 ou 403 est renvoyé.

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

}

avec le fichier de module suivant.

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
la source
2
Merci! J'ai compris mon problème ... il me manquait cette ligne, c'est pourquoi je n'ai catch()pas été trouvée. (smh) import "rxjs/add/operator/catch";
hartpdx
1
Est-il possible d'utiliser le module Routeur pour faire la navigation?
Yuanfei Zhu du
1
Excellente solution pour grouper avec Auth Guard! 1. Auth Guard vérifie l'utilisateur autorisé (par exemple en regardant dans LocalStorage). 2. Lors de la réponse 401/403, vous nettoyez l'utilisateur autorisé pour le Guard (par exemple en supprimant les paramètres correspondants dans LocalStorage). 3. Comme à ce stade précoce, vous ne pouvez pas accéder au routeur pour le transfert vers la page de connexion, l'actualisation de la même page déclenchera les vérifications Guard, qui vous redirigeront vers l'écran de connexion (et conserveront éventuellement votre URL initiale, de sorte que vous " sera transmis à la page demandée après une authentification réussie).
Alex Klaus
1
Hey @NicolasHenneaux - pourquoi pensez-vous qu'il vaut mieux passer outre http? Le seul avantage que je vois est que vous pouvez simplement le mettre en tant que fournisseur: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }alors que lorsque vous surchargez Http, vous devez écrire du code plus gênant useFactoryet vous limiter en appelant `` nouveau '' et en envoyant des arguments spécifiques. WDYT? Une référence à la 2ème méthode: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett - J'ai créé un résumé pour cela qui devrait vous aider: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
82

Angulaire 4.3+

Avec l'introduction de HttpClient est venu la possibilité d'intercepter facilement toutes les demandes / réponses. L'utilisation générale de HttpInterceptors est bien documentée , voyez l'utilisation de base et comment fournir l'intercepteur. Voici un exemple d'un HttpInterceptor qui peut gérer les erreurs 401.

Mis à jour pour 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
            }
        });
    }
}
Le poignard Gilbert Arenas
la source
1
Cela fonctionne-t-il toujours pour vous? Hier, cela fonctionnait pour moi mais après avoir installé d'autres modules, j'obtiens cette erreur: next.handle (…) .do n'est pas une fonction
Multitut
Je pense que celui-ci devrait être utilisé comme une extension de classes comme http est presque toujours une odeur
kboom
1
N'oubliez pas de l'ajouter à votre liste de fournisseurs avec HTTP_INTERCEPTORS. Vous pouvez trouver un exemple dans la documentation
Bruno Peres
2
Génial mais l'utilisation Routerici ne semble pas fonctionner. Par exemple, je souhaite diriger mes utilisateurs vers la page de connexion lorsqu'ils obtiennent un 401-403, mais cela this.router.navigate(['/login']ne fonctionne pas pour moi. Ça ne fait rien
CodyBugstein
Si vous obtenez ".do n'est pas une fonction", ajoutez import 'rxjs/add/operator/do';après avoir importé rxjs.
amoss
19

Comme les API frontales expirent plus rapidement que le lait, avec Angular 6+ et RxJS 5.5+, vous devez utiliser 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);
      })
    );
  }
}

Mise à jour pour Angular 7+ et 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
la source
Je reçois error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.quand le .pipeest là-dedans, aucune erreur lorsque je supprime le.pipe
BlackICE
2
@BlackICE Je suppose que cela confirme la première phrase de ma réponse. J'ai mis à jour avec une réponse pour la dernière version.
Saeb Amini
1
Dans votre exemple ng7 + reqest en fait request- la modification est
trop
12

Le que Observablevous obtenez de chaque méthode de requête est de type Observable<Response>. L' Responseobjet a une statuspropriété qui contiendra le 401SI le serveur a renvoyé ce code. Vous voudrez peut-être le récupérer avant de le mapper ou de le convertir.

Si vous voulez éviter de faire cette fonctionnalité à chaque appel, vous devrez peut-être étendre la Httpclasse d' Angular 2 et injecter votre propre implémentation qui appelle le parent ( super) pour la Httpfonctionnalité normale , puis gérer l' 401erreur avant de renvoyer l'objet.

Voir:

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

Langley
la source
Donc, si j'étends Http, je devrais être en mesure de rediriger vers une route de "connexion" à partir du Http?
pbz le
Voilà la théorie. Vous devrez injecter le routeur dans votre implémentation Http pour le faire.
Langley
Merci de votre aide. J'ai mis à jour la question avec un exemple de code. Je fais probablement quelque chose de mal (étant nouveau sur Angular). Avez-vous des idées de ce que ça pourrait être? Merci.
pbz le
Vous utilisez les fournisseurs Http par défaut, vous devez créer votre propre fournisseur qui résout en une instance de votre classe au lieu de celle par défaut. Voir: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@Langley, merci. Vous avez raison: subscribe ((result) => {}, (error) => {console.log (error.status);}. Le paramètre d'erreur est toujours de type Response.
abedurftig
9

Angulaire 4.3+

Pour compléter la réponse de la dague de Gilbert Arenas :

Si vous avez besoin d'intercepter une erreur, de lui appliquer un traitement et de la transmettre le long de la chaîne (et pas seulement d'ajouter un effet secondaire avec .do), vous pouvez utiliser HttpClient et ses intercepteurs pour faire quelque chose du genre:

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
la source
9

Pour éviter le problème de référencement cyclique causé par l'injection de services tels que "Router" dans une classe dérivée Http, il faut utiliser la méthode Injector post-constructeur. Le code suivant est une implémentation fonctionnelle d'un service Http qui redirige vers la route de connexion chaque fois qu'une API REST renvoie «Token_Expired». Notez qu'il peut être utilisé comme une substitution au Http normal et, en tant que tel, ne nécessite aucune modification des composants ou services déjà existants de votre application.

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

Thoutmose
la source
8

À partir de Angular> = 2.3.0, vous pouvez remplacer le HTTPmodule et injecter vos services. Avant la version 2.3.0, vous ne pouviez pas utiliser vos services injectés en raison d'un bogue principal.

J'ai créé un résumé pour montrer comment c'est fait.

mrgoos
la source
Merci d'avoir rassemblé cela. J'obtenais une erreur de construction qui disait "Impossible de trouver le nom 'Http'" Dans app.module.ts, j'ai donc importé et j'obtiens maintenant l'erreur suivante: "Impossible d'instancier la dépendance cyclique! Http: dans NgModule AppModule"
Bryan
Hey @ Brett, pouvez-vous partager votre app.modulecode? Merci.
mrgoos
Il semble ok. Pouvez-vous ajouter à l'essentiel le HTTP étendu? De plus, importez-vous HTTPailleurs?
mrgoos
Désolé pour le retard. Je suis sur Angular 2.4 maintenant et j'obtiens la même erreur. J'importe Http dans plusieurs fichiers. Voici mon résumé
Bryan
Même problème ici ... On dirait que l'essentiel ne fonctionne pas, alors peut-être devrions-nous le marquer comme tel?
Tuthmosis
2

Angular> 4.3: ErrorHandler pour le service de 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
la source