Обработка 401 в глобальном масштабе с помощью Angular

В моем проекте Angular 2 я вызываю вызовы API из служб, которые возвращают Observable. Затем вызывающий код присоединяется к этому наблюдаемому. Например:

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

Скажем, сервер возвращает 401. Как я могу поймать эту ошибку глобально и перенаправить на страницу/компонент входа?

Спасибо.


Вот что я до сих пор:

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

Сообщение об ошибке, которое я получаю, это "backend.createConnection не является функцией"

Ответ 1

Описание

Лучшее решение, которое я нашел, - это переопределить XHRBackend так, чтобы статус HTTP-ответа 401 и 403 приводил к определенному действию.

Если вы обрабатываете свою аутентификацию вне вашего приложения Angular, вы можете принудительно обновить текущую страницу, чтобы сработал ваш внешний механизм. Я подробно опишу это решение в реализации ниже.

Вы также можете переслать компонент внутри вашего приложения, чтобы ваше приложение Angular не перезагружалось.

Реализация

Угловой> 2.3.0

Благодаря @mrgoos, здесь есть упрощенное решение для Angular 2.3. 0+ из-за исправления ошибки в Angular 2.3.0 (см. Выпуск https://github.com/angular/angular/issues/11606), расширяющего непосредственно Http модуль.

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

Файл модуля теперь содержит только следующий поставщик.

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

Другое решение с помощью маршрутизатора и внешней службы аутентификации подробно описано в следующей сущности по @mrgoos.

Угловой пре 2.3.0

Следующая реализация работает для Angular 2.2.x FINAL и RxJS 5.0.0-beta.12.

Он перенаправляет на текущую страницу (плюс параметр, чтобы получить уникальный URL-адрес и избежать кэширования), если возвращается код HTTP 401 или 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;
    }

}

со следующим файлом модуля.

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

Угловой 4. 3+

С появлением HttpClient появилась возможность легко перехватывать все запросы/ответы. Общее использование HttpInterceptors хорошо документировано, см. Основное использование и как обеспечить перехватчик. Ниже приведен пример HttpInterceptor, который может обрабатывать ошибки 401.

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

Ответ 3

Observable, который вы получаете от каждого метода запроса, имеет тип Observable<Response>. Объект Response имеет свойство status, которое будет содержать 401, если сервер вернет этот код. Поэтому вы можете захотеть получить это, прежде чем отображать его или преобразовать.

Если вы хотите избежать выполнения этой функции при каждом вызове, вам может потребоваться расширить класс Angular 2 Http и добавить свою собственную реализацию, которая вызывает родителя (super) для обычной функции Http а затем обработать ошибку 401 перед возвратом объекта.

См:

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

Ответ 4

Угловой 4. 3+

Чтобы завершить ответ Гилберта Аренаса Кинжала:

Если вам нужно перехватить какую-либо ошибку, применить к ней обработку и направить ее вниз по цепочке (а не просто добавить побочный эффект с помощью .do), вы можете использовать HttpClient и его перехватчики для выполнения чего-то подобного:

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

Ответ 5

От Angular >= 2.3.0 вы можете переопределить модуль HTTP и ввести свои службы. До версии 2.3.0 вы не могли использовать ваши внедренные службы из-за основной ошибки.

Я создал gist, чтобы показать, как это делается.

Ответ 6

Чтобы избежать проблемы с циклическими ссылками, вызванной внедрением таких служб, как "Маршрутизатор", в производный класс Http, необходимо использовать метод инжектора после конструктора. Следующий код представляет собой рабочую реализацию службы Http, которая перенаправляет на маршрут входа в систему каждый раз, когда REST API возвращает "Token_Expired". Обратите внимание, что он может быть использован в качестве замены обычного Http и, следовательно, не требует изменения в вашем приложении уже существующих компонентов или сервисов.

app.module.ts

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

Ответ 7

Angular> 4.3: ErrorHandler для базовой службы

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

Ответ 8

Поскольку API-интерфейсы внешнего интерфейса истекают быстрее, чем молоко, в Angular 6+ и RxJS 5. 5+ вам необходимо использовать 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);
      })
    );
  }
}

Обновление для Angular 7+ и 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: req.url } });
            return of(err as any);
          }
          throw err;
        })
      );
  }
}