AngularのToken認証ライブラリ「angular-token」のInterceptor周りのソースコードを読んでみた

こんにちは!kossyです!




さて、今回はAngularのToken認証ライブラリである、「angular-token」の
Interceptor周りのコードを読んで見たので、ブログに残したいと思います。




環境
Angular 11.0.4
npm 6.14.8
node 12.13.1
TypeScript 4.0.5



Interceptorとは?

本格的にソースコードを読む前に、AngularのInterceptorの仕組みに触れたいと思います。

AngularにはHttp通信用のライブラリとしてHttpClientModuleという組み込みライブラリを提供していますが、
そのHTTP通信のリクエストとレスポンスに介入する仕組みのことをInterceptorと呼んでいます。

主な目的として、リクエストの介入ではHeaderに認証用のtokenを付与することや、
レスポンスの介入ではレスポンスを受け取ったことを知らせるような処理に使われたりします。

Interceptorの実装例はlacolacoさんのzennの記事が非常に詳しかったです。


angular-tokenのinterceptorのソースコードを見る

お待たせしました。それではコードを読んで見ます。

// projects/angular-token/src/lib/angular-token.interceptor.ts

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

import { AngularTokenService } from './angular-token.service';

import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class AngularTokenInterceptor implements HttpInterceptor {

  constructor( private tokenService: AngularTokenService ) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    // Get auth data from local storage
    this.tokenService.getAuthDataFromStorage();

    // Add the headers if the request is going to the configured server
    const authData = this.tokenService.authData.value;

    if (authData &&
      (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) {

      const headers = {
        'access-token': authData.accessToken,
        'client':       authData.client,
        'expiry':       authData.expiry,
        'token-type':   authData.tokenType,
        'uid':          authData.uid
      };

      req = req.clone({
        setHeaders: headers
      });
    }

    return next.handle(req).pipe(tap(
        res => this.handleResponse(res),
        err => this.handleResponse(err)
    ));
  }


  // Parse Auth data from response
  private handleResponse(res: HttpResponse<any> | HttpErrorResponse | HttpEvent<any>): void {
    if (res instanceof HttpResponse || res instanceof HttpErrorResponse) {
      if (this.tokenService.tokenOptions.apiBase === null || (res.url && res.url.match(this.tokenService.tokenOptions.apiBase))) {
        this.tokenService.getAuthHeadersFromResponse(res);
      }
    }
  }
}

改行やコメントアウトを除くと約20行ほどの処理になっています。

まずは17行目を見てみましょう。

// Get auth data from local storage
this.tokenService.getAuthDataFromStorage();

getAuthDataFromStorageメソッドでtokenを取得するコードのようです。
メソッドの中身を見てみましょう。

// projects/angular-token/src/lib/angular-token.service.ts

  // Try to get auth data from storage.
  public getAuthDataFromStorage(): void {

    const authData: AuthData = {
      accessToken:    this.localStorage.getItem('accessToken'),
      client:         this.localStorage.getItem('client'),
      expiry:         this.localStorage.getItem('expiry'),
      tokenType:      this.localStorage.getItem('tokenType'),
      uid:            this.localStorage.getItem('uid')
    };

    if (this.checkAuthData(authData)) {
      this.authData.next(authData);
    }
  }

LocalStorageに保存したAuthDataを取得して、そのAuthDataの中身をチェックしてパスすれば
後続の処理につなげていますね。

checkAuthDataの処理も見てみましょう。

// projects/angular-token/src/lib/angular-token.service.ts

  // このクラス内からしか呼び出せなくて
  // 引数で受け取るauthDataはAuthData型のデータで
  // 返り値はboolean型のデータ

  // Check if auth data complete and if response token is newer
  private checkAuthData(authData: AuthData): boolean { 

    // authDataの各種値が全てnullじゃなければ
    if (
      authData.accessToken != null &&
      authData.client != null &&
      authData.expiry != null &&
      authData.tokenType != null &&
      authData.uid != null
    ) {
      // BehaviorSubjectで定義されたselfが持つauthDataのvalueがnullでなければ
      if (this.authData.value != null) {
        // tokenの有効期限を比較
        return authData.expiry >= this.authData.value.expiry;
      }
      return true;
    }
    return false;
  }

authDataが存在するか、authDataの有効期限が切れていなければtrueが返るコードになってますね。

getAuthDataFromStorageメソッドは、「tokenの有効性をチェックした上でLocalStorageからtokenを取得するコード」でした。

次は20行目です。

// Add the headers if the request is going to the configured server
const authData = this.tokenService.authData.value;

tokenService内のBehaviorSubjectで定義されたauthDataのvalueをconst宣言でauthDataとして定義しています。

BehaviorSubjectの説明も先述のlacolacoさんのtutorialが非常に詳しいです。

次は22行目から42行目までです。

    // tokenService内にSetされたauthDataがあり、apiBaseがnullまたはrequestのurlがapiBaseにmatchすれば
    if (authData &&
      (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) {

      const headers = {
        'access-token': authData.accessToken,
        'client':       authData.client,
        'expiry':       authData.expiry,
        'token-type':   authData.tokenType,
        'uid':          authData.uid
      };

      // requestにheaderを加えたものをcloneして新たなrequestとして定義
      req = req.clone({
        setHeaders: headers
      });
    }

    // handleメソッドにreqを渡して後続の処理を呼び出して、返ってきたobservableオブジェクトにhandleResponseで処理を加える
    return next.handle(req).pipe(tap(
        res => this.handleResponse(res),
        err => this.handleResponse(err)
    ));
}

認証をパスした後、どうやって認証トークンをrequest_headerに忍ばせているんだろう?と思っていましたが、
interceptor内で行っていたんですね。
handleResponseはHTTPリクエストを行った結果をParseするような処理だったため、割愛します。

angular-tokenさん、よしなに実装して下さって感謝です。

まとめ

Interceptorの実装は行数も多くなく処理も複雑でもないので、自分でカスタマイズもできそうですね。
Headerにもっとパラメータ追加した、Responseにログ忍ばせたい、など。




勉強になりました。