Angular④ – インターセプト

シェアする

Angularの説明をこれまでしてきましたが、今回は重要なインターセプトの話になります。
バージョン4から登場したインターセプトですが、利用している人が少ないので驚きます。
というより、利用方法がわからないという人が多いです。

今回はインターセプタの利用方法を紹介します。

インターセプトとは

HTTP Interceptionは@angular/common/httpの主要な機能です。 インターセプトのために、アプリケーションからサーバーへのHTTPリクエストを検査および変換するinterceptorsを宣言します。 また、同じインターセプターは、アプリケーションの途中でサーバーのレスポンスを検査して変換することもできます。 複数のインターセプターは、リクエスト/レスポンスハンドラーの前後のチェインを形成します。

インターセプトは、すべてのHTTPリクエスト/レスポンスに対して、認証からロギングまで、さまざまな暗黙のタスクを一まとまりの標準的な方法で実行できます。

インターセプト無しでは、開発者は各HttpClientメソッド呼び出しに対してこれらのタスクを明示的に行う必要があります。

ドキュメントの引用文ではわかりにくいです。簡単に言えば、途中で割込み、レスポンスなどを変えることができます。共通処理のログの取得、リクエストやレスポンスなどの情報に共通のものを入れるなどの処理ができます。

モジュールでの定義

インターセプトをモジュールに定義します。

  providers: [
    AuthGuard,
    AlertService,
    AuthenticationService,
    UserService,
    { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

    // provider used to create fake backend
    fakeBackendProvider
  ],
  bootstrap: [AppComponent]
})

export class AppModule { }

6行目と7行目:インターセプトの定義です。
provide: HTTP_INTERCEPTORS, useClass: クラス名, multi: true
multi: true は、AngularにHTTP_INTERCEPTORSは単一の値ではなく値の配列を注入するマルチプロバイダーのトークンであることを伝えます。クラス名以外はそのままで大丈夫です。
10行目:これもインターセプタです。違った定義方法です。
説明は下記の fakeBackendProvider を参照してください。

JwtInterceptor

HTTP要求のインターセプトです。
ユーザーがログインしている場合、アプリケーションからHTTP要求をするときにインターセプトします。
AuthorizationヘッダーにJWT認証トークンを追加し、HTTP要求をします。

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // add authorization header with jwt token if available
    let currentUser = JSON.parse(localStorage.getItem('currentUser'));
    if (currentUser && currentUser.token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${currentUser.token}`
        }
      });
    }

    return next.handle(request);
  }
}

8行目から17行目まではインターセプトの処理内容を作成します。
それ以外はクラス名のみを書き換えます。インターセプトの作成の書式になります。
11行目はレスポンスは直接編集ができませんので、Cloneをして書き換えます。
オブザーバーの利用がありますが、次回の記事で内容を紹介します。

ErrorInterceptor

次に紹介したいのはHTTP応答のインタ―セプトです。
HTTP要求に認証のエラーがあるかどうかをチェックします。このソースの場合は401(Unauthorized)応答の場合はユーザはログアウトされ、エラーがスローされて呼び出し元に戻ります。ログアウトされているので、最終的にはログイン画面に遷移させます。

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthenticationService } from '../_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                // auto logout if 401 response returned from api
                this.authenticationService.logout();
                location.reload(true);
            }
            
            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}

13行目でHTTP応答のエラーをインターセプトをしています。
エラーがなければ、インターセプトの対象になりません。
リクエストのパイプラインの利用の仕方などはバージョン5と違いますので気を付けてください。
パイプラインがいろいろと用意されているので応答を拾い出します。

fakeBackendProvider

インターセプトの定義を変えます。サーバとの応答でモックを利用する際に最適なインタ―セプトです。HTTP要求の際、URLなどが一致すれば、ローカルで応答を作り変えています。
また、オフライン環境にしたい場合に応用できます。URLチェック前に、オフラインであることがわかればローカルですべて行えるあお売りケーションにすることも可能です。

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {

  constructor() { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // array in local storage for registered users
    let users: any[] = JSON.parse(localStorage.getItem('users')) || [];

    // wrap in delayed observable to simulate server api call
    return of(null).pipe(mergeMap(() => {

      // authenticate
      if (request.url.endsWith('/users/authenticate') && request.method === 'POST') {
        // find if any user matches login credentials
        let filteredUsers = users.filter(user => {
          return user.username === request.body.username && user.password === request.body.password;
        });

        if (filteredUsers.length) {
          // if login details are valid return 200 OK with user details and fake jwt token
          let user = filteredUsers[0];
          let body = {
            id: user.id,
            username: user.username,
            firstName: user.firstName,
            lastName: user.lastName,
            token: 'fake-jwt-token'
          };

          return of(new HttpResponse({ status: 200, body: body }));
        } else {
          // else return 400 bad request
          return throwError({ error: { message: 'ユーザー名かパスワードが間違っています' } });
        }
      }

      // get users
      if (request.url.endsWith('/users') && request.method === 'GET') {
        // check for fake auth token in header and return users if valid, this security is implemented server side in a real application
        if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') {
          return of(new HttpResponse({ status: 200, body: users }));
        } else {
          // トークンが無効かない場合は401を返します。401でログオフしログ画面に遷移させます。
          return throwError({ status: 401, error: { message: '無許可' } });
        }
      }

      // get user by id
      if (request.url.match(/\/users\/\d+$/) && request.method === 'GET') {
        // check for fake auth token in header and return user if valid, this security is implemented server side in a real application
        if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') {
          // find user by id in users array
          let urlParts = request.url.split('/');
          let id = parseInt(urlParts[urlParts.length - 1]);
          let matchedUsers = users.filter(user => { return user.id === id; });
          let user = matchedUsers.length ? matchedUsers[0] : null;

          return of(new HttpResponse({ status: 200, body: user }));
        } else {
          // トークンが無効かない場合は401を返します。401でログオフしログ画面に遷移させます。
          return throwError({ status: 401, error: { message: '無許可' } });
        }
      }

      // register user
      if (request.url.endsWith('/users/register') && request.method === 'POST') {
        // get new user object from post body
        let newUser = request.body;

        // validation
        let duplicateUser = users.filter(user => { return user.username === newUser.username; }).length;
        if (duplicateUser) {
          return throwError({ error: { message: 'Username "' + newUser.username + '" 既にトークンがあります' } });
        }

        // save new user
        newUser.id = users.length + 1;
        users.push(newUser);
        localStorage.setItem('users', JSON.stringify(users));

        // respond 200 OK
        return of(new HttpResponse({ status: 200 }));
      }

      // delete user
      if (request.url.match(/\/users\/\d+$/) && request.method === 'DELETE') {
        // check for fake auth token in header and return user if valid, this security is implemented server side in a real application
        if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') {
          // find user by id in users array
          let urlParts = request.url.split('/');
          let id = parseInt(urlParts[urlParts.length - 1]);
          for (let i = 0; i < users.length; i++) {
            let user = users[i];
            if (user.id === id) {
              // delete user
              users.splice(i, 1);
              localStorage.setItem('users', JSON.stringify(users));
              break;
            }
          }

          // respond 200 OK
          return of(new HttpResponse({ status: 200 }));
        } else {
          // トークンが無効かない場合は401を返します。401でログオフしログ画面に遷移させます。
          return throwError({ status: 401, error: { message: '無許可' } });
        }
      }

      // 今回は、上記のURLに該当しないものはサーバに接続します。
      return next.handle(request);

    }))

      // RxJS/issues/648対応です。 (https://github.com/Reactive-Extensions/RxJS/issues/648)
      .pipe(materialize())
      .pipe(delay(500))
      .pipe(dematerialize());
  }
}
// インターセプトの定義
export let fakeBackendProvider = {
  // use fake backend in place of Http service for backend-less development
  provide: HTTP_INTERCEPTORS,
  useClass: FakeBackendInterceptor,
  multi: true
};

128~132行目にインターセプトの定義があります。
テスト用などに使う際にこのような形にしてモジュールに定義をすると便利です。不用になれば削除します。
Angularの書式が多くあります。基本的な書式を覚えると、いろいろな応用ができます。

まとめ

インターセプタはHTTP要求やHTTP応答をインターセプトし、共通処理を挿入できます。
HTTP要求の際の誰が何をしたのかというセキュリティ関係のログを作成する場合にも便利です。利用価値が高いので、いろいろと工夫をしてください。
オフライン環境ネットを遣えない場合の環境などにも適用できます。

シェアする

フォローする