Lumen の Auth・Guard・User 認証徹底解説(3)— 流れがぜんぶ解けた!(前編)

Wake
By Wake2019年8月8日約 15 分で読了
Lumen の Auth・Guard・User 認証徹底解説(3)— 流れがぜんぶ解けた!(前編)

大きなフレームワークやシステムに比べると、僕は小さくて軽い、差し替えのきくパッケージのほうが好みです。Idiorm & ParisPhinx のような。まず、構造が理解しやすく、抽象化の層が少ない。そしてプロジェクトにとっては、各機能の結合度も下がるので、必要なときにそのまま差し替えたり調整したりできます(たとえば、あるパッケージがメンテされなくなったとき)。

もちろん欠点もあります。何でも自分で手作業でつなぎ込む必要があるし、独立したパッケージはコアシステムとの相性がそこまでよくなかったり、パッケージどうしがぶつかったり(たとえば、共通の依存を違うバージョンで要求している場合)します。しかも、一つのプロジェクトの中にいろいろな設計思想が入り混じるので、一つずつ理解していかないといけません。

それでも、いまの主要なフレームワークの設計思想は、大いに参考にする価値があります。システムがある規模まで育つと、アーキテクチャの抽象化やインターフェース化で整理し、柔軟性を高めることが、どうしても必要になります。これは避けられない発展の道すじです。

Auth・Guard・User、実際の流れ

本題に戻りましょう。ここからは、認証の全行程を一度ていねいにたどっていきます。図には Lumen と AuthServiceProvider の 2 つを書き足して、つながりを見やすくしました。

Lumen → ServiceProvider → AuthManager

FullBasicFlow

Lumen で認証機能を有効にするには、まず AuthServiceProviderLumen Application —— つまり $app —— に登録する必要があります。ここが流れの出発点でもあります:

$app->register(\App\Providers\AuthServiceProvider::class);

AuthServiceProvider は、関連するサービスをどうやってシステムに結びつけているのでしょう?ソースを見ると、boot メソッド に次のようにあります:

$this->app['auth']->viaRequest('api', function ($request) {
  ...
});

まず、ここで $this->app['auth'] を取得しようとしています。app['auth'] は本来存在しないオブジェクトで、アクセスされたのをきっかけに、AuthManager がインスタンス化されます。でも、なぜ Illuminate\Auth\AuthManager なのでしょう?これは、コアの Illuminate\Auth\AuthServiceProvider が指定しているものです。具体的な流れは次のとおりです:

  1. コードが app['auth'] にアクセスする。
  2. appauth がすでに作られているかを自分でチェックし、まだなら、インスタンス化する。
  3. Lumen Application —— Laravel\Lumen\Application —— が auth を特別なキーワードとしてバインドしているので、auth をインスタンス化しようとすると registerAuthBindings 関数が呼ばれる。
  4. registerAuthBindings 関数が、コアサービスの Illuminate\Auth\AuthServiceProvider を登録する。
  5. Illuminate\Auth\AuthServiceProvider が、その register メソッド の中で、シングルトンパターン app->singleton を使って Illuminate\Auth\AuthManager クラスをインスタンス化し、app['auth'] に割り当てる。

というわけで、app['auth'] を呼び出して少し手を加えるくらいで、App\Providers\AuthServiceProvider 自体はとくに何かサービスを読み込んでいるわけではありません。処理の大半は、コアの auth まわりのバインドが担っています。

後ろの app['auth']->viaRequest というコードは Guard に関わるので、次の節でまとめて説明します。

AuthManager → XXX (Guard) → GuardDriver → Guard

FullBasicFlow

では、AuthManager はどうやって Guard とやり取りするのでしょう?Lumen には、3 種類の Guard クラスが組み込まれています:

  • RequestGuard:リクエストをベースにした、拡張用の認証モード。オブジェクト生成時に callback 関数を渡す必要があります。この callback は $requestuserProvider の 2 つを受け取り、使えるユーザーオブジェクト を返すことが求められます(認証結果を返すだけではありません)。
  • TokenGuard:同じくリクエストをベースに、渡された内容で認証するモード。たとえば header に特定の token や password が入っているか、などです。
  • SessionGuard:Session(と Cookie)のデータをもとに認証するモード。ふつうの PHP プログラムで、ユーザーのログインを記録・識別するときに使う、あのやり方です。

app['auth'] —— AuthManager がインスタンス化されれば、それを通じて、上の Guard オブジェクト(あるいは自作の Guard)を取得できます:

  1. app['auth']->guard('XXX') を呼ぶ。XXX という名前の Guard オブジェクトがすでにあれば、それを返す。なければ、Guard オブジェクトを作る準備に入る。名前を渡さないときは、config/auth.phpdefaults.guard が自動で使われる点に注意。
  2. Guard オブジェクトを作る前に、config/auth.phpguards に XXX Guard の設定があるかをまず確認する。なければエラーを投げる。
  3. XXX Guard の設定をまとめて、この Guard に次のものがあるかを順に確認する:
    • カスタムの callback 生成関数:コード内で app['auth']->extend() を使い callback で定義した生成関数。この定義の名前を、設定ファイルにも設定しておく必要がある。
    • AuthManager 内のドライバメソッド
      • AuthManager を自分で拡張していれば、createXXXDriver というメソッドを定義して XXX Guard を動かせる。
      • 組み込み Guard の デフォルトのドライバメソッド も同じ方式で、たとえば SessionGuardcreateSessionDriver、TokenGuard は createTokenDriver です。
  4. 生成関数/ドライバメソッド を呼んで対象の Guard をインスタンス化し、そのオブジェクトを XXX をキーにして AuthManager 自身の guards array に入れる。
  5. Guard オブジェクトを返す。

この流れから分かるとおり、自分で Guard を定義したいなら、いちばん簡単なのは app['auth']->extend() で Guard を生成する関数を定義し、config/auth.php に設定を書いておくことです。

app['auth']->viaRequest に戻ると、viaRequest は実のところ RequestGuard をベースにした extend() を応用したものです。かみくだいて言えば、RequestGuard オブジェクトを作ってくれて、user callback を渡すための関数を用意してくれる、というものです。この 2 つのコードを見てみましょう:

AuthServiceProviderboot メソッド

$this->app['auth']->viaRequest('api', function ($request) {
  if ($request->input('api_token')) {
    return User::where('api_token', $request->input('api_token'))->first();
  }
});

AuthManagerviaRequest

public function viaRequest($driver, callable $callback) {
  return $this->extend($driver, function () use ($callback) {

    $guard = new RequestGuard($callback, $this->app['request'], $this->createUserProvider());

    $this->app->refresh('request', $guard, 'setRequest');

    return $guard;
  });
}

AuthServiceProvider が起動すると、viaRequest()extend() を通じて api という名前の生成/ドライバメソッドを定義し、リクエストの api_token をデータベースと突き合わせてユーザーを取得する callback を、RequestGuard のコンストラクタ引数として渡します。次に app['auth']->guard('api') が呼ばれたときにインスタンス化され、callback にしたがってユーザーオブジェクトを返します。

おわりに

AuthManager は、認証アーキテクチャの中でいちばん役回りが複雑なオブジェクトでしょう。本来の機能に加えて、__call を使って Guard への動的な呼び出しも実装しています。つまり、app['auth']->guard()->guardFunc() の代わりに app['auth']->guardFunc() と直接書ける、ということです。これ、本当にアーキテクチャを追いかけづらいんですよね…

このシリーズ、書けば書くほど増えていく気がします。次の記事では、AuthManager と UserProvider の関係にもう少し踏み込んでいきます。読んでくださって、ありがとうございます。また次回に。

カテゴリー:
Wake

Wake

著者について

趣味が高じてフロントエンドとバックエンドの技術を少しかじっているエンジニアです。大好きなプログラミングのほかに、バドミントン、ボードゲーム、読書、料理、ピアノも好きです。今は AI も積極的に使っています。