🐘

【Laravel】Auth::user() の裏側の動作を順を追って理解する

2023/10/09に公開

はじめに

Laravel では、Auth::user() を使うことで、以下のように手軽に認証情報にアクセスすることができます。

routes/web.php
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::get('/test', function () {
  var_dump(Auth::user());
})->middleware('auth');
出力結果
array(6) {
    ["id"] => int(1)
    ["name"] => string(9) "Test User"
    ["email"] => string(16) "test@example.com"
    ["email_verified_at"] => string(27) "2023-10-04T12:21:02.000000Z"
    ["created_at"] => string(27) "2023-10-04T12:21:02.000000Z"
    ["updated_at"] => string(27) "2023-10-04T12:21:02.000000Z"
}

今回はこちらの Auth::user() のコードリーディングをしてみます。
Laravel のファサードや、認証機能の仕組みについて理解を深めることが目的です。

手元に Laravel のコードがあれば、ぜひ一緒に追ってみてください。

前提条件

  • Laravel 10.10
  • 認証設定(config/auth.php)
    • driver: session
    • provider: Eloquent(App\Models\User
  • セッション設定(config/session.php)
    • driver: file

標準的な Cookie + セッション形式の認証、セッション情報はファイルに保存する設定です。

内部実装の調査 ① Auth ~ AuthManager

早速ですが、Auth::user() の内部実装を追っていきます。

まずは Illuminate\Support\Facades\Authuser() という static メソッドを探してみますが、見つけることができません。
継承元の Illuminate\Support\Facades\Facade にも定義されていないようです。
実装はどこにあるのでしょうか。

このような場合、クラスに __callStatic() が定義されていれば、代わりにそちらの処理が呼ばれます。
__callStatic() の説明は[1]を参照)

vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
/**
 * Handle dynamic, static calls to the object.
 *
 * @param  string  $method
 * @param  array  $args
 * @return mixed
 *
 * @throws \RuntimeException
 */
public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

$instance に対して、$method で指定された名前のメソッド(今回の場合 $method = 'user')を実行しているようですね。

今回は、$instance には Illuminate\Auth\AuthManager のインスタンスがセットされているため、そちらのクラスの user() メソッドが呼ばれることになります。

なぜ AuthManager$instance にセットされるかは、Illuminate\Support\Facades\FacadegetFacadeRoot(), resolveFacadeInstance() あたりの実装から確認することができます
ただし、各ファサードの具体実装箇所は 公式サイト に記載があるため、毎回コード内容を追う必要はありません。

内部実装の調査 ② AuthManager ~ SessionGuard

Illuminate\Auth\AuthManageruser() が呼ばれることまで分かりましたが、こちらでもそのようなメソッドを見つけることができません。

代わりに __call() が定義されているためこちらが呼ばれるようです。
__call() の説明は[2]を参照)

vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php
/**
  * Dynamically call the default driver instance.
  *
  * @param  string  $method
  * @param  array  $parameters
  * @return mixed
  */
public function __call($method, $parameters)
{
    return $this->guard()->{$method}(...$parameters);
}

$this->guard() で取得できるインスタンスに対して、$method で指定される名前のメソッド(今回の場合は $method = 'user')を実行していることが分かります。

今回は config/auth.php にて、認証方式をセッションに設定しているため、$this->guard() からは Illuminate\Auth\SessionGuard のインスタンスが取得されます。
(具体的な取得過程は、同クラス内の guard(), resolve(), createSessionDriver() あたりの実装にて確認できます)

すなわち、Illuminate\Auth\SessionGuarduser() メソッドが呼ばれることになります。

内部実装の調査 ③ SessionGuard

Illuminate\Auth\SessionGuard の内部を確認してみると、ようやく user() の実装を見つけることができました。

vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
/**
 * Get the currently authenticated user.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function user()
{
    if ($this->loggedOut) {
        return;
    }

    // If we've already retrieved the user for the current request we can just
    // return it back immediately. We do not want to fetch the user data on
    // every call to this method because that would be tremendously slow.
    if (! is_null($this->user)) {
        return $this->user;
    }

    $id = $this->session->get($this->getName());

    // First we will try to load the user using the identifier in the session if
    // one exists. Otherwise we will check for a "remember me" cookie in this
    // request, and if one exists, attempt to retrieve the user using that.
    if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }

    // If the user is null, but we decrypt a "recaller" cookie we can attempt to
    // pull the user data on that cookie which serves as a remember cookie on
    // the application. Once we have a user we can return it to the caller.
    if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
        $this->user = $this->userFromRecaller($recaller);

        if ($this->user) {
            $this->updateSession($this->user->getAuthIdentifier());

            $this->fireLoginEvent($this->user, true);
        }
    }

    return $this->user;
}

コード中のコメント内容等踏まえると、次の二つがメイン処理のようです。

a. $id = $this->session->get($this->getName())(セッションからユーザ ID の取得?)
b. $this->provider->retrieveById($id)(ユーザ ID を元にユーザ情報の取得?)

この二つの処理をもう少し掘り下げてみたいと思います。

a. $id = $this->session->get($this->getName())

$this->session->get()Illuminate\Contracts\Session\Session (インターフェース)の get() メソッドに該当します。

vendor/laravel/framework/src/Illuminate/Contracts/Session/Session.php
/**
 * Get an item from the session.
 *
 * @param  string  $key
 * @param  mixed  $default
 * @return mixed
 */
public function get($key, $default = null);

セッションから$key (キー)に該当する値を引っ張ってくるメソッドのようです。
具体実装がどのようになっているか気になるところですが、長くなりそうなため末尾の補足に記載としました。

次に $this->getName() の実装も確認してみます。

vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
/**
 * The name of the guard. Typically "web".
 *
 * Corresponds to guard name in authentication configuration.
 *
 * @var string
 */
public readonly string $name;

// 中略

/**
 * Get a unique identifier for the auth session value.
 *
 * @return string
 */
public function getName()
{
    return 'login_'.$this->name.'_'.sha1(static::class);
}

login + ガード名 + クラス名のハッシュ値 をアンダースコアつなぎで取得するメソッドのようです。
(例: login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d

以上から、$id = $this->session->get($this->getName())
「セッションからキー login_web_xxxxxxx に対応する値を取得してくる処理」
と解釈できそうです。

今回は冒頭の 前提条件 に記載したように、セッションをファイルに保存する設定にしているため、実際にそのファイルの中身を確認してみます(ファイル名はセッションごとに変わります)。

storage/framework/sessions/2J7ZnF37f3oPINIJwxj8cvTKAPwqNYV7PZvBWGyh
a:5:{s:6:"_token";s:40:"dZDspN1ziS4bnVFeIKgt6VH417y8iETs3o4KYE8n";s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}s:9:"_previous";a:1:{s:3:"url";s:22:"http://localhost/login";}s:3:"url";a:0:{}s:50:"login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d";i:1;}

このままでは読み取れないためデシリアライズしてみます。

デシリアライズされたセッション情報
$ php artisan tinker
Psy Shell v0.11.21 (PHP 8.2.10 — cli) by Justin Hileman
> unserialize('a:5:{s:6:"_token";s:40:"dZD...(以下略)')
= [
    "_token" => "dZDspN1ziS4bnVFeIKgt6VH417y8iETs3o4KYE8n",
    "_flash" => [
      "old" => [],
      "new" => [],
    ],
    "_previous" => [
      "url" => "http://localhost/login",
    ],
    "url" => [],
    "login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d" => 1,
  ]

確かに、login_web_xxxxxxx のキー名で値が設定されていました。
整数の 1 が値として設定されていますね(認証済ユーザの ID と想定されます)。

b. $this->provider->retrieveById($id)

a. の処理で取得した $id をこちらの処理に渡すことになります。
Illuminate\Contracts\Auth\UserProvider (インターフェース)の retrieveById() メソッドに該当します。

vendor/laravel/framework/src/Illuminate/Contracts/Auth/UserProvider.php
/**
 * Retrieve a user by their unique identifier.
 *
 * @param  mixed  $identifier
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function retrieveById($identifier);

DocBlock の記載内容から、ID に応じてユーザを取得してくる処理と分かります。

具体実装も確認してみます。
今回は config/auth.php にて provider の設定を Eloquent にしているため、実装クラスは Illuminate\Auth\EloquentUserProvider が引き受けます。

vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php
/**
 * Retrieve a user by their unique identifier.
 *
 * @param  mixed  $identifier
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function retrieveById($identifier)
{
    $model = $this->createModel();

    return $this->newModelQuery($model)
                ->where($model->getAuthIdentifierName(), $identifier)
                ->first();
}

カスタマイズのために色々と変数化されていますが、動作内容は以下のコードと大差ないはずです。

User::where('id', $identifier)->first()

DB から id カラムをキーに、ユーザ情報を引っ張ってくる処理です。
こちらが最終的に Auth::user() の実行結果として取得できるのですね。

まとめ

Auth::user() の裏側の動作

  • セッション形式の認証の場合、Illuminate\Auth\SessionGuarduser() メソッドが呼ばれる
  • user() メソッドでは主に以下が行われる
    1. セッションからキー login_web_xxxxxxx に対応する値を取得する(認証済ユーザ ID の取得)
    2. 取得したユーザ ID を元に UserProvider からユーザ情報を取得する

Auth::user() は非常に簡単に使える反面、動作の仕組みを理解せずとも使えてしまうため、いざというときの認証機能改修やカスタマイズに対応することができません。
また、要件定義や設計などの業務でも認証機能の動作原理理解が求められるはずです。

今後もコードリーディングを通して、認証機能のみならずソフトウェアの動作原理に対する理解を確かなものにしていきたいです。

補足

Illuminate\Contracts\Session\Session@get メソッドの具体実装について

以下のコードにありました。

vendor/laravel/framework/src/Illuminate/Session/Store.php
/**
 * The session attributes.
 *
 * @var array
 */
protected $attributes = [];

// 中略

/**
 * Get an item from the session.
 *
 * @param  string  $key
 * @param  mixed  $default
 * @return mixed
 */
public function get($key, $default = null)
{
    return Arr::get($this->attributes, $key, $default);
}

$attributes プロパティから $key に対応する値を取り出しているだけのようです。
$attributes プロパティに値を設定する過程が知りたいです。

若干天下り的ですが、以下のステップで設定されるようです。

① リクエスト時に Illuminate\Session\Middleware\StartSession@handle が呼ばれる
App\Http\Kernel にて標準で呼ばれる設定のミドルウェアです。

Illuminate\Session\Middleware\StartSession@getSession が呼ばれ、Cookie 中のセッション ID の値を読み込む
標準では laravel_session というキーで設定されています

Illuminate\Session\Middleware\StartSession@handleStatefulRequest, @startSession を経由して Illuminate\Session\Store@start メソッドが呼ばれる

vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php
/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    // 中略

    $session = $this->getSession($request);

    // 中略

    return $this->handleStatefulRequest($request, $session, $next);
}

// 中略

/**
 * Get the session implementation from the manager.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Contracts\Session\Session
 */
public function getSession(Request $request)
{
    return tap($this->manager->driver(), function ($session) use ($request) {
        $session->setId($request->cookies->get($session->getName()));
    });
}

/**
 * Handle the given request within session state.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Contracts\Session\Session  $session
 * @param  \Closure  $next
 * @return mixed
 */
protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
    // If a session driver has been configured, we will need to start the session here
    // so that the data is ready for an application. Note that the Laravel sessions
    // do not make use of PHP "native" sessions in any way since they are crappy.
    $request->setLaravelSession(
        $this->startSession($request, $session)
    );

    // 中略
}

/**
 * Start the session for the given request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Contracts\Session\Session  $session
 * @return \Illuminate\Contracts\Session\Session
 */
protected function startSession(Request $request, $session)
{
    return tap($session, function ($session) use ($request) {
        //中略

        $session->start();
    });
}

Illuminate\Session\Store@start から @loadSession 経由で @readFromHandler が呼ばれる

⑤ (@readFromHandler 内) \SessionHandlerInterface@read メソッドに、セッション情報の読み込みを委譲する
今回の設定では、実装クラスは Illuminate\Session\FileSessionHandler

⑥ (@readFromHandler 内) 読み込んだセッションをデシリアライズする

⑦ (@loadSession 内) $attributes プロパティに @readFromHandler メソッドから返却されたセッション情報を設定する

vendor/laravel/framework/src/Illuminate/Session/Store.php
/**
 * Start the session, reading the data from a handler.
 *
 * @return bool
 */
public function start()
{
    $this->loadSession();

    // 中略
}

/**
 * Load the session data from the handler.
 *
 * @return void
 */
protected function loadSession()
{
    $this->attributes = array_merge($this->attributes, $this->readFromHandler());

    $this->marshalErrorBag();
}

/**
 * Read the session data from the handler.
 *
 * @return array
 */
protected function readFromHandler()
{
    if ($data = $this->handler->read($this->getId())) {
        if ($this->serialization === 'json') {
            $data = json_decode($this->prepareForUnserialize($data), true);
        } else {
            $data = @unserialize($this->prepareForUnserialize($data));
        }

        if ($data !== false && is_array($data)) {
            return $data;
        }
    }

    return [];
}
vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php
/**
 * The filesystem instance.
 *
 * @var \Illuminate\Filesystem\Filesystem
 */
protected $files;

/**
 * The path where sessions should be stored.
 *
 * @var string
 */
protected $path;

// 中略

/**
 * {@inheritdoc}
 *
 * @return string|false
 */
public function read($sessionId): string|false
{
    if ($this->files->isFile($path = $this->path.'/'.$sessionId) &&
        $this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
        return $this->files->sharedGet($path);
    }

    return '';
}

セッション情報が Illuminate\Session\Store に読み込まれる($attributes プロパティに設定される)までの過程を理解することができました。

脚注
  1. https://www.php.net/manual/ja/language.oop5.overloading.php#object.callstatic ↩︎

  2. https://www.php.net/manual/ja/language.oop5.overloading.php#object.call ↩︎

Discussion