Angularで401をグローバルに処理する


90

私のAngular 2プロジェクトでは、Observableを返すサービスからAPI呼び出しを行います。次に、呼び出しコードはこのオブザーバブルをサブスクライブします。例えば:

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
私はこれがあなたに小さな指針を
Pankaj Parkar

回答:


78

説明

私が見つけた最良の解決策は、上書きすることであるXHRBackendようにHTTP応答ステータス401403、特定のアクションにリードを。

Angularアプリケーションの外部で認証を処理する場合、外部メカニズムがトリガーされるように現在のページを強制的に更新できます。このソリューションについては、以下の実装で詳しく説明します。

Angularアプリケーションがリロードされないように、アプリケーション内のコンポーネントに転送することもできます。

実装

角度> 2.3.0

@mrgoosのおかげで、モジュールを直接拡張するangular 2.3.0(バグhttps://github.com/angular/angular/issues/11606を参照)のバグ修正により、angular 2.3.0+の簡略化されたソリューションがここにあり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による次の要点で詳しく説明されています。

Angular 2.3.0より前

次の実装はAngular 2.2.x FINALおよびで機能しRxJS 5.0.0-beta.12ます。

HTTPコード401または403が返された場合、現在のページ(および一意のURLを取得してキャッシュを回避するためのパラメーター)にリダイレクトします。

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 2016

1
ルーターモジュールを使用してナビゲーションを行うことはできますか?
Yuanfei Zhu

1
Auth Guardとのバンドルに最適なソリューションです。1. Auth Guardは、許可されたユーザーをチェックします(LocalStorageを調べるなど)。2. 401/403応答で、Guardの承認済みユーザーをクリーンアップします(LocalStorageの対応するパラメーターを削除するなど)。3.この初期段階では、ルーターにアクセスしてログインページに転送できないため、同じページを更新するとGuardのチェックがトリガーされ、ログイン画面に転送されます(オプションで初期URLを保持するため、認証が成功した後、要求されたページに転送されます)。
Alex Klaus

1
ちょっと@NicolasHenneaux-オーバーライドするよりも良いと思うのはなぜhttpですか?私が目にする唯一の利点は、それをプロバイダーとして単純に置くことができるということです{ provide: XHRBackend, useClass: AuthenticationConnectionBackend }。Httpをオーバーライドするときは、より厄介なコードを記述useFactoryし、「new」を呼び出して特定の引数を送信することで自分自身を制限する必要があります。WDYT?2番目の方法への参照:adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos

3
@ブレット-あなたのために役立つはずの要旨を作成しました:gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos

82

Angular 4.3以降

HttpClientの導入により、すべての要求/応答を簡単にインターセプトする機能が登場しました。HttpInterceptorsの一般的な使用法は十分に文書化されています。基本的な使用法とインターセプターを提供する方法を参照してください。以下は、401エラーを処理できるHttpInterceptorの例です。

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はほとんど常に匂いであるように私はこの1つは、クラスの拡張として使用されるべきだと思う
kboom

1
HTTP_INTERCEPTORSを使用してプロバイダーリストに追加することを忘れないでください。ドキュメントの
Bruno Peres '20年

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それは私の答えの最初の文を再確認していると思います。最新バージョンの回答で更新しました。
Saeb Amini、

1
あなたのng7 +の例でreqは、実際にはrequest-編集は私が行うには
小さすぎ

12

Observableあなたはそれぞれのリクエストメソッドから取得型ですObservable<Response>Responseオブジェクトは、持ってstatus保持するプロパティ401サーバはそのコードを返した場合に。そのため、マッピングまたは変換する前にそれを取得したい場合があります。

呼び出しごとにこの機能を実行したくない場合は、Angular 2のHttpクラスを拡張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
@Langley、ありがとう。そのとおりです:subscribe((result)=> {}、(error)=> {console.log(error.status);}。エラーパラメータのタイプはまだResponseです
abedurftig

9

Angular 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派生クラスに注入されることによって発生する循環参照の問題を回避するには、ポストコンストラクターインジェクターメソッドを使用する必要があります。次のコードは、REST APIが「Token_Expired」を返すたびにログインルートにリダイレクトするHttpサービスの実際の実装です。これは通常のHttpの代替として使用できるため、アプリケーションの既存のコンポーネントまたはサービスで何かを変更する必要がないことに注意してください。

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


8

Angular> = 2.3.0以降では、HTTPモジュールをオーバーライドしてサービスを注入できます。バージョン2.3.0より前では、コアバグのため、挿入されたサービスを使用できませんでした。

方法を示す要点を作成しました。


まとめてくれてありがとう。app.module.tsで「名前「Http」が見つかりません」というビルドエラーが発生したため、インポートして次のエラーを取得しています:「循環依存関係をインスタンス化できません!Http:NgModule AppModule内」
Bryan

@ブレットさん、app.moduleコードを共有できますか?ありがとう。
mrgoos 2016

大丈夫そうです。拡張HTTPを要点に追加できますか?また、HTTP他の場所にインポートしますか?
mrgoos 2016

遅れて申し訳ありません。現在Angular 2.4を使用していますが、同じエラーが発生します。複数のファイルにHttpをインポートします。ここに私の更新要旨は次のとおりです。gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
ブライアン・

ここで同じ問題...この要点が機能していないように見えるので、おそらくそれをそのようにマークする必要がありますか?
血栓症

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);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.