【Laravel】Auth::user() の裏側の動作を順を追って理解する
はじめに
Laravel では、Auth::user()
を使うことで、以下のように手軽に認証情報にアクセスすることができます。
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\Auth
の user()
という static メソッドを探してみますが、見つけることができません。
継承元の Illuminate\Support\Facades\Facade
にも定義されていないようです。
実装はどこにあるのでしょうか。
このような場合、クラスに __callStatic()
が定義されていれば、代わりにそちらの処理が呼ばれます。
(__callStatic()
の説明は[1]を参照)
/**
* 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\Facade
の getFacadeRoot()
, resolveFacadeInstance()
あたりの実装から確認することができます
ただし、各ファサードの具体実装箇所は 公式サイト に記載があるため、毎回コード内容を追う必要はありません。
内部実装の調査 ② AuthManager ~ SessionGuard
Illuminate\Auth\AuthManager
の user()
が呼ばれることまで分かりましたが、こちらでもそのようなメソッドを見つけることができません。
代わりに __call()
が定義されているためこちらが呼ばれるようです。
(__call()
の説明は[2]を参照)
/**
* 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\SessionGuard
の user()
メソッドが呼ばれることになります。
内部実装の調査 ③ SessionGuard
Illuminate\Auth\SessionGuard
の内部を確認してみると、ようやく user()
の実装を見つけることができました。
/**
* 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 を元にユーザ情報の取得?)
この二つの処理をもう少し掘り下げてみたいと思います。
$id = $this->session->get($this->getName())
a. $this->session->get()
は Illuminate\Contracts\Session\Session
(インターフェース)の get()
メソッドに該当します。
/**
* Get an item from the session.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null);
セッションから$key
(キー)に該当する値を引っ張ってくるメソッドのようです。
具体実装がどのようになっているか気になるところですが、長くなりそうなため末尾の補足に記載としました。
次に $this->getName()
の実装も確認してみます。
/**
* 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
に対応する値を取得してくる処理」
と解釈できそうです。
今回は冒頭の 前提条件 に記載したように、セッションをファイルに保存する設定にしているため、実際にそのファイルの中身を確認してみます(ファイル名はセッションごとに変わります)。
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 と想定されます)。
$this->provider->retrieveById($id)
b. a. の処理で取得した $id
をこちらの処理に渡すことになります。
Illuminate\Contracts\Auth\UserProvider
(インターフェース)の retrieveById()
メソッドに該当します。
/**
* 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
が引き受けます。
/**
* 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\SessionGuard
のuser()
メソッドが呼ばれる -
user()
メソッドでは主に以下が行われる- セッションからキー
login_web_xxxxxxx
に対応する値を取得する(認証済ユーザ ID の取得) - 取得したユーザ ID を元に UserProvider からユーザ情報を取得する
- セッションからキー
Auth::user()
は非常に簡単に使える反面、動作の仕組みを理解せずとも使えてしまうため、いざというときの認証機能改修やカスタマイズに対応することができません。
また、要件定義や設計などの業務でも認証機能の動作原理理解が求められるはずです。
今後もコードリーディングを通して、認証機能のみならずソフトウェアの動作原理に対する理解を確かなものにしていきたいです。
補足
Illuminate\Contracts\Session\Session@get
メソッドの具体実装について
以下のコードにありました。
/**
* 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
メソッドが呼ばれる
/**
* 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
メソッドから返却されたセッション情報を設定する
/**
* 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 [];
}
/**
* 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
プロパティに設定される)までの過程を理解することができました。
Discussion