Menangani 401 secara global dengan Angular

92

Dalam proyek Angular 2 saya, saya membuat panggilan API dari layanan yang mengembalikan Observable. Kode panggilan kemudian berlangganan observable ini. Sebagai contoh:

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

Katakanlah server mengembalikan 401. Bagaimana saya bisa menangkap kesalahan ini secara global dan mengarahkan ke halaman / komponen login?

Terima kasih.


Inilah yang saya miliki sejauh ini:

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

Pesan kesalahan yang saya dapatkan adalah "backend.createConnection bukan fungsi"

pbz
sumber
1
Saya pikir ini bisa memberi Anda sedikit petunjuk
Pankaj Parkar

Jawaban:

79

Deskripsi

Solusi terbaik yang saya temukan adalah menimpa XHRBackendstatus respons HTTP 401dan 403mengarah ke tindakan tertentu.

Jika Anda menangani otentikasi Anda di luar aplikasi Angular Anda, Anda dapat memaksa menyegarkan halaman saat ini sehingga mekanisme eksternal Anda dipicu. Saya merinci solusi ini dalam implementasi di bawah ini.

Anda juga bisa meneruskan ke komponen di dalam aplikasi Anda sehingga aplikasi Angular Anda tidak dimuat ulang.

Penerapan

Sudut> 2.3.0

Berkat @mrgoos, berikut adalah solusi yang disederhanakan untuk angular 2.3.0+ karena perbaikan bug di angular 2.3.0 (lihat masalah https://github.com/angular/angular/issues/11606 ) memperluas Httpmodul secara langsung .

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

File modul sekarang hanya berisi penyedia berikut.

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

Solusi lain yang menggunakan Router dan layanan otentikasi eksternal dirinci dalam inti berikut oleh @mrgoos.

Angular sebelum 2.3.0

Implementasi berikut bekerja untuk Angular 2.2.x FINALdan RxJS 5.0.0-beta.12.

Ini mengalihkan ke halaman saat ini (ditambah parameter untuk mendapatkan URL unik dan menghindari caching) jika kode HTTP 401 atau 403 dikembalikan.

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

}

dengan file modul berikut.

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
sumber
2
Terima kasih! Saya menemukan masalah saya ... Saya melewatkan baris ini, itulah sebabnya catch()tidak ditemukan. (smh) import "rxjs/add/operator/catch";
hartpdx
1
Apakah mungkin menggunakan modul Router untuk melakukan navigasi?
Yuanfei Zhu
1
Solusi hebat untuk bundling dengan Auth Guard! 1. Auth Guard memeriksa pengguna yang berwenang (misalnya dengan melihat ke LocalStorage). 2. Pada respons 401/403 Anda membersihkan pengguna resmi untuk Guard (misalnya dengan menghapus parameter yang sesuai di Penyimpanan Lokal). 3. Karena pada tahap awal ini Anda tidak dapat mengakses Router untuk diteruskan ke halaman login, menyegarkan halaman yang sama akan memicu pemeriksaan Penjaga, yang akan meneruskan Anda ke layar login (dan secara opsional mempertahankan URL awal Anda, jadi Anda ' akan diteruskan ke halaman yang diminta setelah otentikasi berhasil).
Alex Klaus
1
Halo @NicolasHenneaux - menurut Anda mengapa lebih baik menimpa http? Satu-satunya keuntungan yang saya lihat adalah Anda dapat dengan mudah menempatkannya sebagai penyedia: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }sementara saat menimpa Http Anda perlu menulis kode yang lebih canggung seperti useFactorydan membatasi diri Anda dengan memanggil 'baru' dan mengirimkan argumen khusus. WDYT? Referensi ke metode ke-2: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett - Saya telah membuat intinya yang akan membantu Anda: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
84

Angular 4.3+

Dengan diperkenalkannya HttpClient muncul kemampuan untuk dengan mudah mencegat semua permintaan / tanggapan. Penggunaan umum HttpInterceptors didokumentasikan dengan baik , lihat penggunaan dasar dan cara menyediakan interseptor. Di bawah ini adalah contoh HttpInterceptor yang dapat menangani kesalahan 401.

Diperbarui untuk 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
            }
        });
    }
}
Belati Arena Gilbert
sumber
1
Apakah ini masih berhasil untuk Anda? Kemarin itu berfungsi untuk saya tetapi setelah menginstal modul lain, saya mendapatkan kesalahan ini: next.handle (…) .do bukan fungsi
Multitut
Saya pikir yang satu ini harus digunakan sebagai ekstensi kelas seperti http hampir selalu bau
kboom
1
Jangan lupa untuk menambahkannya ke daftar penyedia Anda dengan HTTP_INTERCEPTORS. Anda dapat menemukan contohnya di dokumen
Bruno Peres
2
Bagus tapi menggunakan Routerdi sini sepertinya tidak berhasil. Misalnya, saya ingin mengarahkan pengguna saya ke halaman masuk saat mereka mendapatkan 401-403, tetapi this.router.navigate(['/login']tidak berfungsi untuk saya. Itu tidak melakukan apa
CodyBugstein
Jika Anda mendapatkan ".do is not a function", tambahkan import 'rxjs/add/operator/do';setelah Anda mengimpor rxjs.
amoss
20

Karena API frontend kedaluwarsa lebih cepat daripada susu, dengan Angular 6+ dan RxJS 5.5+, Anda perlu menggunakan 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);
      })
    );
  }
}

Pembaruan untuk Angular 7+ dan 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
sumber
Saya mendapatkan error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.ketika .pipeada di sana, tidak ada kesalahan ketika saya menghapus.pipe
BlackICE
2
@BlackICE Saya rasa itu menegaskan kembali kalimat pertama dalam jawaban saya. Saya telah memperbarui dengan jawaban untuk versi terbaru.
Saeb Amini
1
Dalam contoh ng7 + Anda reqsebenarnya request- pengeditan terlalu kecil untuk saya buat
ask_io
12

Yang ObservableAnda dapatkan dari setiap metode permintaan adalah tipe Observable<Response>. The Responseobjek, memiliki statusproperti yang akan memegang 401JIKA server kembali kode tersebut. Jadi, Anda mungkin ingin mengambilnya sebelum memetakan atau mengonversinya.

Jika Anda ingin menghindari melakukan fungsionalitas ini pada setiap panggilan, Anda mungkin harus memperluas Httpkelas Angular 2 dan memasukkan implementasi Anda sendiri yang memanggil induk ( super) untuk Httpfungsionalitas reguler dan kemudian menangani 401kesalahan sebelum mengembalikan objek.

Lihat:

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

Langley
sumber
Jadi jika saya memperpanjang Http maka saya harus bisa mengarahkan ke rute "login" dari dalam Http?
pbz
Itulah teorinya. Anda harus menyuntikkan router ke dalam implementasi Http Anda untuk melakukannya.
Langley
Terima kasih atas bantuan Anda. Saya telah memperbarui pertanyaan dengan kode contoh. Saya mungkin melakukan sesuatu yang salah (menjadi baru di Angular). Tahu apa itu? Terima kasih.
pbz
Anda menggunakan penyedia Http default, Anda harus membuat penyedia Anda sendiri yang menentukan instance kelas Anda, bukan yang default. Lihat: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@Lang, terima kasih. Anda benar: subscribe ((result) => {}, (error) => {console.log (error.status);}. Parameter error masih bertipe Respon.
abedurftig
9

Angular 4.3+

Untuk melengkapi jawaban The Gilbert Arenas Dagger :

Jika yang Anda butuhkan adalah mencegat kesalahan apa pun, terapkan perawatan untuk itu dan teruskan ke bawah rantai (dan tidak hanya menambahkan efek samping dengan .do), Anda dapat menggunakan HttpClient dan interseptornya untuk melakukan hal semacam itu:

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
sumber
9

Untuk menghindari masalah referensi siklik yang disebabkan oleh layanan seperti "Router" yang dimasukkan ke dalam kelas turunan Http, seseorang harus menggunakan metode Injector pasca-konstruktor. Kode berikut adalah implementasi yang berfungsi dari layanan Http yang mengarahkan ke rute Login setiap kali REST API mengembalikan "Token_Expired". Perhatikan bahwa ini bisa digunakan sebagai pengganti Http biasa dan dengan demikian, tidak perlu mengubah apa pun dalam komponen atau layanan aplikasi Anda yang sudah ada.

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

Tuthmosis
sumber
8

Dari Angular> = 2.3.0 Anda dapat mengganti HTTPmodul dan memasukkan layanan Anda. Sebelum versi 2.3.0, Anda tidak dapat menggunakan layanan yang diinjeksi karena bug inti.

Saya telah membuat inti untuk menunjukkan bagaimana hal itu dilakukan.

mrgoos
sumber
Terima kasih telah menyatukannya. Saya mendapatkan kesalahan pembuatan yang mengatakan "Tidak dapat menemukan nama 'Http'" Di app.module.ts, jadi saya mengimpor dan sekarang mendapatkan kesalahan berikut: "Tidak dapat membuat contoh ketergantungan siklik! Http: di NgModule AppModule"
Bryan
Hai @ Brett- dapatkah Anda membagikan app.modulekode Anda ? Terima kasih.
mrgoos
Sepertinya oke. Dapatkah Anda menambahkan inti HTTP yang diperpanjang? Selain itu, apakah Anda mengimpor di HTTPtempat lain?
mrgoos
Maaf atas keterlambatannya. Saya menggunakan Angular 2.4 sekarang dan mendapatkan kesalahan yang sama. Saya mengimpor Http dalam beberapa file. Ini inti terbaru saya: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan
Masalah yang sama di sini ... Sepertinya inti ini tidak berfungsi jadi mungkin kita harus menandainya seperti itu?
Tuthmosis
2

Angular> 4.3: ErrorHandler untuk layanan dasar

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
sumber