Обробка 401-х років у всьому світі за допомогою Angular


90

У своєму проекті 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
Я думаю, що це може дати вам трохи вказівника
Панкадж Паркар

Відповіді:


78

Опис

Найкраще рішення, яке я знайшов, - це перевизначення 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
Дякую! Я зрозумів свою проблему ... Мені не вистачало цього рядка, саме тому catch()його не знайшли. (smh) import "rxjs/add/operator/catch";
hartpdx

1
Чи можна використовувати модуль маршрутизатора для навігації?
Yuanfei Zhu

1
Чудове рішення для поєднання з Auth Guard! 1. Auth Guard перевіряє авторизованого користувача (наприклад, переглядаючи LocalStorage). 2. У відповіді 401/403 ви очищаєте авторизованого користувача для Guard (наприклад, видаляючи відповідні параметри в LocalStorage). 3. Оскільки на цьому ранній стадії ви не можете отримати доступ до маршрутизатора для переадресації на сторінку входу, оновлення тієї ж сторінки викличе перевірки Guard, які перенаправлять вас на екран входу (і, за бажанням, збережуть вашу початкову URL-адресу, щоб ви буде переадресовано на потрібну сторінку після успішної автентифікації).
Алекс Клаус,

1
Привіт @NicolasHenneaux - чому ти вважаєш, що краще тоді перевизначити http? Єдина перевага, яку я бачу, полягає в тому, що ви можете просто поставити його як провайдера: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }тоді як при перевизначенні Http вам потрібно писати більш незграбний код, наприклад, useFactoryі обмежувати себе, викликаючи 'new' і надсилаючи конкретні аргументи. WDYT? Посилання на 2-й метод: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos

3
@Brett - Я створив для нього суть, яка повинна вам допомогти: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos

82

Кутова 4.3+

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

Оновлено для 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
Це все ще працює для вас? Вчора він працював для мене, але після встановлення інших модулів я отримую таку помилку: next.handle (…) .do не є функцією
Multitut

Я думаю, що цей слід використовувати як розширення класів, як http майже завжди запах
kboom

1
Не забудьте додати його до списку своїх провайдерів за допомогою HTTP_INTERCEPTORS. Ви можете знайти приклад у документах
Бруно Перес

2
Чудово, але використання Routerтут, здається, не працює. Наприклад, я хочу направити своїх користувачів на сторінку входу, коли вони отримують 401-403, але this.router.navigate(['/login']не працює для мене. Це нічого не робить
CodyBugstein

Якщо ви отримуєте ".do не є функцією", додайте import 'rxjs/add/operator/do';після імпорту rxjs.
amoss

19

Оскільки інтерфейсні 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: request.url } });
            return of(err as any);
          }
          throw err;
        })
      );
  }
}

Я отримую, error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.коли .pipeтам все, помилок при видаленні.pipe
BlackICE

2
@BlackICE Я думаю, це підтверджує перше речення у моїй відповіді. Я оновив відповідь на останню версію.
Саїб Amini

1
У вашому прикладі ng7 + reqнасправді request- редагування для мене невелике
ask_io

12

ObservableВи отримуєте від кожного методу запиту має типу Observable<Response>. ResponseОб'єкт має statusвластивість , яке буде тримати 401IF сервер повернув цей код. Отже, вам може знадобитися отримати це перед картографуванням або перетворенням.

Якщо ви хочете уникати виконання цієї функціональності під час кожного виклику, можливо, вам доведеться розширити Httpклас Angular 2 та ввести власну реалізацію, яка викликає батьківський ( super) для звичайної Httpфункціональності, а потім обробити 401помилку перед поверненням об'єкта.

Побачити:

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


Отже, якщо я розширюю Http, то я повинен мати можливість перенаправляти на маршрут "входу" з Http?
pbz

Це теорія. Для цього вам доведеться ввести маршрутизатор у вашу реалізацію Http.
Ленглі

Спасибі за вашу допомогу. Я оновив запитання зразком коду. Я, мабуть, роблю щось не так (будучи новим у Angular). Будь-яка ідея, що це може бути? Дякую.
pbz

Ви використовуєте провайдери Http за замовчуванням, вам потрібно створити власного провайдера, який переходить до екземпляра вашого класу, а не до типового. Див .: angular.io/docs/ts/latest/api/core/Provider-class.html
Ленглі

1
@ Ленглі, дякую. Ви маєте рацію: підпишіться ((результат) => {}, (помилка) => {console.log (error.status);}. Параметр помилки все ще має тип Відповідь.
abedurftig

9

Кутова 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'));
        });
    }
}

9

Щоб уникнути проблеми з циклічним посиланням, спричиненої введенням таких служб, як «Маршрутизатор», у похідний клас Http, слід використовувати метод інжектора після конструктора. Наступний код - це робоча реалізація служби Http, яка перенаправляє на маршрут входу щоразу, коли REST API повертає "Token_Expired". Зверніть увагу, що він може бути використаний як заміна звичайного Http і як такий не вимагає нічого змінювати у вже існуючих компонентах або службах вашої програми.

app.module.ts

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

розширений-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

З Angular> = 2.3.0 ви можете замінити HTTPмодуль та ввести свої послуги. До версії 2.3.0 ви не могли користуватися введеними службами через основну помилку.

Я створив суть, щоб показати, як це робиться.


Дякую за те, що це разом. Я отримував помилку збірки із написом "Не вдається знайти ім'я 'Http'" у app.module.ts, тому я імпортував та отримую таку помилку: "Не вдається створити циклічну залежність! Http: у NgModule AppModule"
Брайан,

Привіт @ Бретт - ти можеш поділитися своїм app.moduleкодом? Дякую.
mrgoos

Здається, добре. Чи можете ви додати до суті розширений HTTP? Крім того, чи імпортуєте ви HTTPще куди-небудь?
mrgoos

Вибачте за затримку. Зараз я перебуваю на Angular 2.4 і отримую ту ж помилку. Я імпортую Http у декілька файлів. Ось мій оновлений суть: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Брайан,

Те саме питання тут ... Схоже, ця суть не працює, то, можливо, ми повинні позначити це як таке?
Tuthmosis

2

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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.