Xử lý 401 trên toàn cầu với Angular


90

Trong dự án Angular 2 của tôi, tôi thực hiện các lệnh gọi API từ các dịch vụ trả về một Observable. Mã gọi sau đó đăng ký vào điều này có thể quan sát được. Ví dụ:

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

Giả sử máy chủ trả về 401. Làm cách nào để tôi có thể bắt lỗi này trên toàn cầu và chuyển hướng đến trang / thành phần đăng nhập?

Cảm ơn.


Đây là những gì tôi có cho đến nay:

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

Thông báo lỗi mà tôi nhận được là "backend.createConnection không phải là một hàm"


1
Tôi nghĩ điều này có thể cung cấp cho bạn một
gợi ý

Câu trả lời:


78

Sự miêu tả

Giải pháp tốt nhất mà tôi đã tìm thấy là ghi đè XHRBackendtrạng thái phản hồi HTTP 401403dẫn đến một hành động cụ thể.

Nếu bạn xử lý xác thực của mình bên ngoài ứng dụng Angular, bạn có thể buộc làm mới trang hiện tại để cơ chế bên ngoài của bạn được kích hoạt. Tôi trình bày chi tiết giải pháp này trong phần triển khai bên dưới.

Bạn cũng có thể chuyển tiếp đến một thành phần bên trong ứng dụng của mình để ứng dụng Angular của bạn không bị tải lại.

Thực hiện

Góc> 2.3.0

Cảm ơn @mrgoos, đây là giải pháp đơn giản hóa cho góc 2.3.0+ do sửa lỗi trong góc 2.3.0 (xem sự cố https://github.com/angular/angular/issues/11606 ) mở rộng trực tiếp Httpmô-đun.

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

Tệp mô-đun bây giờ chỉ chứa trình cung cấp sau.

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

Một giải pháp khác sử dụng Bộ định tuyến và dịch vụ xác thực bên ngoài được @mrgoos trình bày chi tiết trong ý kiến sau .

Angular trước 2.3.0

Việc triển khai sau đây hoạt động cho Angular 2.2.x FINALRxJS 5.0.0-beta.12.

Nó chuyển hướng đến trang hiện tại (cộng với một tham số để lấy URL duy nhất và tránh bộ nhớ đệm) nếu trả về mã HTTP 401 hoặc 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;
    }

}

với tệp mô-đun sau.

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
Cảm ơn! Tôi đã tìm ra vấn đề của mình ... Tôi đã thiếu dòng này, đó là lý do tại sao catch()không tìm thấy. (SMH) import "rxjs/add/operator/catch";
hartpdx

1
Có thể sử dụng mô-đun Bộ định tuyến để điều hướng không?
Yuanfei Zhu

1
Giải pháp tuyệt vời để kết hợp với Auth Guard! 1. Auth Guard kiểm tra người dùng được ủy quyền (ví dụ: bằng cách xem LocalStorage). 2. Trên phản hồi 401/403, bạn xóa người dùng được ủy quyền cho Guard (ví dụ: bằng cách xóa các tham số cốt lõi tương ứng trong LocalStorage). 3. Vì ở giai đoạn đầu này, bạn không thể truy cập Bộ định tuyến để chuyển tiếp đến trang đăng nhập, việc làm mới cùng một trang sẽ kích hoạt kiểm tra Bảo vệ, sẽ chuyển tiếp bạn đến màn hình đăng nhập (và tùy chọn giữ nguyên URL ban đầu của bạn, vì vậy bạn ' sẽ được chuyển tiếp đến trang được yêu cầu sau khi xác thực thành công).
Alex Klaus

1
Xin chào @NicolasHenneaux - tại sao bạn nghĩ rằng tốt hơn hết là ghi đè http? Lợi ích duy nhất mà tôi thấy là bạn có thể đơn giản đặt nó như một nhà cung cấp: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }trong khi khi ghi đè Http, bạn cần viết mã khó xử hơn như useFactoryvà giới hạn bản thân bằng cách gọi 'mới' và gửi các đối số cụ thể. WDYT? Một tham chiếu đến phương pháp thứ 2: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos

3
@Brett - Tôi đã tạo ý chính cho nó sẽ giúp bạn: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos 12/12/16

82

Angular 4.3+

Với sự ra đời của HttpClient đã mang đến khả năng dễ dàng chặn tất cả các yêu cầu / phản hồi. Cách sử dụng chung của HttpInterceptors đã được ghi lại đầy đủ , hãy xem cách sử dụng cơ bản và cách cung cấp interceptor. Dưới đây là ví dụ về HttpInterceptor có thể xử lý lỗi 401.

Đã cập nhật cho 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
Điều này vẫn làm việc cho bạn? Hôm qua nó đã làm việc cho tôi, nhưng sau khi cài đặt các module khác, tôi nhận được lỗi này: next.handle (...) .do không phải là một chức năng
Multitut

Tôi nghĩ cái này nên được sử dụng như phần mở rộng của lớp học như http là hầu như luôn luôn có mùi
KBOOM

1
Đừng quên thêm nó vào danh sách nhà cung cấp của bạn với HTTP_INTERCEPTORS. Bạn có thể tìm thấy một ví dụ trong tài liệu
Bruno Peres

2
Tuyệt vời nhưng sử dụng Routerở đây dường như không hoạt động. Ví dụ: tôi muốn định tuyến người dùng của mình đến trang đăng nhập khi họ nhận được 401-403, nhưng this.router.navigate(['/login']không hoạt động với tôi. Nó không làm gì
CodyBugstein

Nếu bạn nhận được ".do không phải là một hàm", hãy thêm import 'rxjs/add/operator/do';sau khi bạn nhập rxjs.
amoss

19

Vì các API giao diện người dùng hết hạn nhanh hơn sữa, với Angular 6+ và RxJS 5.5+, bạn cần sử dụng 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);
      })
    );
  }
}

Cập nhật cho Angular 7+ và 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;
        })
      );
  }
}

Tôi nhận được error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.khi .pipeđang ở trong đó, không có lỗi khi tôi tháo.pipe
BlackICE

2
@BlackICE Tôi đoán điều đó khẳng định lại câu đầu tiên trong câu trả lời của tôi. Tôi đã cập nhật câu trả lời cho phiên bản mới nhất.
Saeb Amini

1
Trong ví dụ ng7 + của bạn reqthực sự là request- bản chỉnh sửa là nhỏ để tôi thực hiện
ask_io

12

Các Observablebạn nhận được từ mỗi phương thức yêu cầu là loại Observable<Response>. Đối Responsetượng, có một thuộc statustính sẽ giữ 401IF máy chủ trả về mã đó. Vì vậy, bạn có thể muốn lấy nó trước khi ánh xạ nó hoặc chuyển đổi nó.

Nếu bạn muốn tránh thực hiện chức năng này trong mỗi lần gọi, bạn có thể phải mở rộng Httplớp của Angular 2 và đưa vào triển khai của riêng bạn đối với nó gọi hàm cha ( super) cho Httpchức năng thông thường và sau đó xử lý 401lỗi trước khi trả lại đối tượng.

Xem:

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


Vì vậy, nếu tôi mở rộng Http thì tôi có thể chuyển hướng đến một tuyến đường "đăng nhập" từ bên trong Http không?
pbz

Đó là lý thuyết. Bạn sẽ phải đưa bộ định tuyến vào triển khai Http của mình để thực hiện.
Langley

Cảm ơn bạn đã giúp đỡ. Tôi đã cập nhật câu hỏi bằng mã mẫu. Có lẽ tôi đang làm sai điều gì đó (mới sử dụng Angular). Bất kỳ ý tưởng những gì nó có thể là? Cảm ơn.
pbz

Bạn đang sử dụng các nhà cung cấp Http mặc định, bạn phải tạo trình cung cấp của riêng mình để phân giải một phiên bản của lớp thay vì phiên bản mặc định. Xem: angle.io/docs/ts/latest/api/core/Provider-class.html
Langley

1
@Langley, cảm ơn. Bạn đúng: subscribe ((kết quả) => {}, (error) => {console.log (error.status);}. Thông số lỗi vẫn thuộc loại Phản hồi.
abedurftig Ngày

9

Angular 4.3+

Để hoàn thành câu trả lời của The Gilbert Arenas Dagger :

Nếu những gì bạn cần là chặn bất kỳ lỗi nào, hãy áp dụng cách xử lý và chuyển tiếp lỗi đó xuống chuỗi (và không chỉ thêm tác dụng phụ với .do), bạn có thể sử dụng HttpClient và các bộ chặn của nó để thực hiện một số việc:

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

Để tránh sự cố tham chiếu theo chu kỳ do các dịch vụ như "Bộ định tuyến" được đưa vào một lớp dẫn xuất Http, người ta phải sử dụng phương thức Injector hậu khởi tạo. Đoạn mã sau là cách triển khai hoạt động của dịch vụ Http chuyển hướng đến tuyến Đăng nhập mỗi khi API REST trả về "Token_Expired". Lưu ý rằng nó có thể được sử dụng để thay thế cho Http thông thường và như vậy, không yêu cầu thay đổi bất kỳ điều gì trong các thành phần hoặc dịch vụ hiện có của ứng dụng của bạn.

app.module.ts

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

extension-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

Từ Angular> = 2.3.0, bạn có thể ghi đè HTTPmô-đun và chèn các dịch vụ của mình. Trước phiên bản 2.3.0, bạn không thể sử dụng các dịch vụ được đưa vào do một lỗi chính.

Tôi đã tạo một ý chính để hiển thị cách nó được thực hiện.


Cảm ơn vì đã đặt điều đó lại với nhau. Tôi gặp lỗi bản dựng cho biết "Không thể tìm thấy tên 'Http'" Trong app.module.ts, vì vậy tôi đã nhập và hiện gặp lỗi sau: "Không thể khởi tạo phụ thuộc theo chu kỳ! Http: trong NgModule AppModule"
Bryan

Này @ Brett- bạn có thể chia sẻ app.modulemã của mình không? Cảm ơn.
mrgoos

Nó có vẻ ổn. Bạn có thể thêm vào ý chính của HTTP mở rộng không? Ngoài ra bạn có nhập ở HTTPđâu nữa không?
mrgoos 27/12/16

Xin lỗi về sự chậm trễ. Tôi đang sử dụng Angular 2.4 bây giờ và gặp lỗi tương tự. Tôi nhập Http trong một số tệp. Đây là ý chính cập nhật của tôi: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan

Vấn đề tương tự ở đây ... Có vẻ như ý chính này không hoạt động vì vậy có lẽ chúng ta nên đánh dấu nó là như vậy?
Tuthmosis

2

Angular> 4.3: ErrorHandler cho dịch vụ cơ sở

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);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.