Laravel Breezeのソースを読んだので超絶詳しく解説していく【Remember me編】
はじめに
この記事は前回の【ログイン処理編】の続きです。
前回の【ログイン処理編】で軽く触れた「Remember me」のチェックを入れた際の動作について見て行きます。
↑ の画像の赤枠部分がそれにあたります。
Laravel Breeze とは?
Laravel Breeze は、簡単に認証機能を追加することができる Laravel のパッケージです。
Breeze は Laravel 8.x のバージョンから導入され、Jetstream のような高度な機能を提供しない代わりに、より簡素なインストールと認証プロセスを提供しています。
1. Remember me とログイン
まずは復習です。
Remember me のチェックを ON にした状態でログインすると、以下の 2 つの処理が実行されます。
- $this->ensureRememberTokenIsSet($user);
- $this->queueRecallerCookie($user);
/**
* Log a user into the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param bool $remember
* @return void
*/
public function login(AuthenticatableContract $user, $remember = false)
{
$this->updateSession($user->getAuthIdentifier());
// If the user should be permanently "remembered" by the application we will
// queue a permanent cookie that contains the encrypted copy of the user
// identifier. We will then decrypt this later to retrieve the users.
if ($remember) {
$this->ensureRememberTokenIsSet($user);
$this->queueRecallerCookie($user);
}
// If we have an event dispatcher instance set we will fire an event so that
// any listeners will hook into the authentication events and run actions
// based on the login and logout events fired from the guard instances.
$this->fireLoginEvent($user, $remember);
$this->setUser($user);
}
/**
* Create a new "remember me" token for the user if one doesn't already exist.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function ensureRememberTokenIsSet(AuthenticatableContract $user)
{
if (empty($user->getRememberToken())) {
$this->cycleRememberToken($user);
}
}
/**
* Refresh the "remember me" token for the user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function cycleRememberToken(AuthenticatableContract $user)
{
$user->setRememberToken($token = Str::random(60));
$this->provider->updateRememberToken($user, $token);
}
/**
* Queue the recaller cookie into the cookie jar.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function queueRecallerCookie(AuthenticatableContract $user)
{
$this->getCookieJar()->queue($this->createRecaller(
$user->getAuthIdentifier() . '|' . $user->getRememberToken() . '|' . $user->getAuthPassword()
));
}
/**
* Create a "remember me" cookie for a given ID.
*
* @param string $value
* @return \Symfony\Component\HttpFoundation\Cookie
*/
protected function createRecaller($value)
{
return $this->getCookieJar()->make($this->getRecallerName(), $value, $this->getRememberDuration());
}
/**
* Get the cookie creator instance used by the guard.
*
* @return \Illuminate\Contracts\Cookie\QueueingFactory
*
* @throws \RuntimeException
*/
public function getCookieJar()
{
if (! isset($this->cookie)) {
throw new RuntimeException('Cookie jar has not been set.');
}
return $this->cookie;
}
$this->ensureRememberTokenIsSet($user);
この処理では、生成したトークンを User テーブルに保存しています。
cycleRememberToken 内部の$this->provider->updateRememberToken($user, $token);の provider は初期設定だと EloquentUserProvider となります。
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(UserContract $user, $token)
{
$user->setRememberToken($token);
$timestamps = $user->timestamps;
$user->timestamps = false;
$user->save();
$user->timestamps = $timestamps;
}
$this->queueRecallerCookie($user);
一方で、こちらはユーザー ID、生成したトークン、パスワードをクッキーに保存しています。
/**
* Queue the recaller cookie into the cookie jar.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function queueRecallerCookie(AuthenticatableContract $user)
{
$this->getCookieJar()->queue($this->createRecaller(
$user->getAuthIdentifier() . '|' . $user->getRememberToken() . '|' . $user->getAuthPassword()
));
}
/**
* Create a "remember me" cookie for a given ID.
*
* @param string $value
* @return \Symfony\Component\HttpFoundation\Cookie
*/
protected function createRecaller($value)
{
return $this->getCookieJar()->make($this->getRecallerName(), $value, $this->getRememberDuration());
}
/**
* Get the cookie creator instance used by the guard.
*
* @return \Illuminate\Contracts\Cookie\QueueingFactory
*
* @throws \RuntimeException
*/
public function getCookieJar()
{
if (! isset($this->cookie)) {
throw new RuntimeException('Cookie jar has not been set.');
}
return $this->cookie;
}
で、前回はここでトークンを生成して、DB とクッキーに保存してるっぽいというところまで把握したのですが、「ではそれがどう使われているのか?」が今回のテーマです。
2. Remember me の使い所
Remember me のチェックを ON にして、クッキーに保存されたログイン情報は一体どのタイミングで使われているのでしょうか?
おそらくですが、「ログイン情報を Remember me」というからにはログイン後に Web サーバー(Laravel)と通信が発生するタイミングで使われていそうなことは想像に難くないですよね。
というわけで、安直ですが画面遷移が発生するタイミングに注目してみたいと思います。
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
これはルーティングを定義している routes/web.php の一部なのですが、middleware('auth')とあります。
middleware('auth')が何を指すかは app/Http/Kernel.php を見るとわかります。
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
はい、わかりましたね?
\App\Http\Middleware\Authenticate::class が auth ミドルウェアの実体です。
ミドルウェアがどのように動いているのか?については、こちらの記事を読んでみてください。
3. App\Http\Middleware\Authenticate
ということで、App\Http\Middleware\Authenticate の中をみてみます。
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}
はい、ぱっと見で違和感ありまくりですよね?
なぜなら、handle メソッドを持っていないからです。
「ではどこに持っているのか?」を考えると、どうやらこのクラスは Middleware のサブクラスのようで、その Middleware の実体は Illuminate\Auth\Middleware\Authenticate のようです。
では、Illuminate\Auth\Middleware\Authenticate を見てみましょう。
4. Illuminate\Auth\Middleware\Authenticate
抜粋ですが、確かに handle メソッドが存在していることを確認できました。
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string[] ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
そして、auth ミドルウェアの処理の本体は、同じクラス内に定義された authenticate とわかります。
この authenticate で重要なのは、$this->auth->guard($guard)->check()です。
このとき、$this->auth は vendor/laravel/framework/src/Illuminate/Auth/AuthManager が入っています。
ということで、vendor/laravel/framework/src/Illuminate/Auth/AuthManager の guard を確認します。
5. vendor/laravel/framework/src/Illuminate/Auth/AuthManager
guard メソッドの中身は以下の通りです。
/**
* Attempt to get the guard from the local cache.
*
* @param string|null $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}
/**
* Get the default authentication driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['auth.defaults.guard'];
}
ここで、引数の name は null で来るので、name は this->getDefaultDriver()の戻り値です。
すると、app['config']['auth.defaults.guard']とのことから、config/auth.php を見ます。
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
以上から、name には'web'が代入されます。
そして最終的に return されるのは$this->resolve($name)の戻り値です。
では resolve の中をみます。
/**
* Resolve the given guard.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($name, $config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($name, $config);
}
throw new InvalidArgumentException(
"Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
);
}
/**
* Get the guard configuration.
*
* @param string $name
* @return array
*/
protected function getConfig($name)
{
return $this->app['config']["auth.guards.{$name}"];
}
config にセットされるのは、先の config/auth.php より session となります。
その結果、$driverMethod にセットされるのは、"createSessionDriver"となり、$this->createSessionDriver の実行を試みます。
/**
* Create a session based authentication guard.
*
* @param string $name
* @param array $config
* @return \Illuminate\Auth\SessionGuard
*/
public function createSessionDriver($name, $config)
{
$provider = $this->createUserProvider($config['provider'] ?? null);
$guard = new SessionGuard(
$name,
$provider,
$this->app['session.store'],
);
// When using the remember me functionality of the authentication services we
// will need to be set the encryption instance of the guard, which allows
// secure, encrypted cookie values to get generated for those cookies.
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($this->app['cookie']);
}
if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($this->app['events']);
}
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}
if (isset($config['remember'])) {
$guard->setRememberDuration($config['remember']);
}
return $guard;
}
ここで this の createUserProvider が呼ばれていますが、AuthManager 自体は createUserProvider を持ってはいません。
use CreatesUserProviders;がクラスの先頭で宣言されているので、この createUserProvider は CreatesUserProviders トレイトに定義されています。
/**
* Create the user provider implementation for the driver.
*
* @param string|null $provider
* @return \Illuminate\Contracts\Auth\UserProvider|null
*
* @throws \InvalidArgumentException
*/
public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
return match ($driver) {
'database' => $this->createDatabaseProvider($config),
'eloquent' => $this->createEloquentProvider($config),
default => throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
),
};
}
/**
* Create an instance of the Eloquent user provider.
*
* @param array $config
* @return \Illuminate\Auth\EloquentUserProvider
*/
protected function createEloquentProvider($config)
{
return new EloquentUserProvider($this->app['hash'], $config['model']);
}
でここは特にややこしい話はなにもないので説明を端折り結論だけ言います。
今の config/auth.php の設定だと、$this->createEloquentProvider($config)が返ります。
そしてその実体とは、\Illuminate\Auth\EloquentUserProvider です。
つまり、最終的に guard として返って行くのは、\Illuminate\Auth\EloquentUserProvider を使った SessionGuard ということです。
6. vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
コードが深いところまで入っていったので、「そもそも何の話してたっけ?」というところなのですが、元はといえばこの$this->auth->guard($guard)->check()の話でした。
そして今は、$this->auth->guard($guard) = vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php というところまできています。
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
つまり、次に実行されるのは vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php の check です。
しかし、vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php には check というメソッドはなく、代わりに GuardHelpers トレイトが定義されています。
そう、先ほどの AuthManager と同様に、SessionGuard に対して check メソッドを実行すると、GuardHelpers トレイトの check が実行されます。
/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{
return ! is_null($this->user());
}
で、この$this は SessionGuard です。
今度はトレイトにはなく、SessionGuard に定義された user()を見ることになります。
ここで、ようやく Remember me の本題に入ることになります前振り長すぎだろ…
/**
* 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;
}
user()のなかではいろいろとやっていますが、Remember me が絡んでいるのがどこかというと、ここです。
// 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);
}
}
user 情報が null となり、しかし recaller が null でない場合、なにやら user 情報を再び代入しているようです。
/**
* Get the decrypted recaller cookie for the request.
*
* @return \Illuminate\Auth\Recaller|null
*/
protected function recaller()
{
if (is_null($this->request)) {
return;
}
if ($recaller = $this->request->cookies->get($this->getRecallerName())) {
return new Recaller($recaller);
}
}
そして recaller ではなにやら cookie から値を取り出しています。
さて、ここで前回の記事である【ログイン編】でこのようなコードが登場しました。
以下のコードは、Remember me が true(チェック ON)の場合に実行されるコードです。
/**
* Queue the recaller cookie into the cookie jar.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function queueRecallerCookie(AuthenticatableContract $user)
{
$this->getCookieJar()->queue($this->createRecaller(
$user->getAuthIdentifier() . '|' . $user->getRememberToken() . '|' . $user->getAuthPassword()
));
}
/**
* Create a "remember me" cookie for a given ID.
*
* @param string $value
* @return \Symfony\Component\HttpFoundation\Cookie
*/
protected function createRecaller($value)
{
return $this->getCookieJar()->make($this->getRecallerName(), $value, $this->getRememberDuration());
}
このことから、$recaller が null でない場合、ここに入ってくるのはパイプ区切りの id,token,password のようです。
そうすると、if 文の中へ入って行き、userFromRecaller($recaller)が実行されます。
// 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);
}
}
/**
* Pull a user from the repository by its "remember me" cookie token.
*
* @param \Illuminate\Auth\Recaller $recaller
* @return mixed
*/
protected function userFromRecaller($recaller)
{
if (! $recaller->valid() || $this->recallAttempted) {
return;
}
// 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.
$this->recallAttempted = true;
$this->viaRemember = ! is_null($user = $this->provider->retrieveByToken(
$recaller->id(), $recaller->token()
));
return $user;
}
そうすると、ユーザー ID とトークンと使って、$this->provider から user を取得しているようです。
ここで$this->provider なにか、先ほど確認しましたね?
そう、\Illuminate\Auth\EloquentUserProvider です。
では、\Illuminate\Auth\EloquentUserProvider の retrieveByToken を確認します。(何やってるか想像できそうですが)
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
$retrievedModel = $this->newModelQuery($model)->where(
$model->getAuthIdentifierName(), $identifier
)->first();
if (! $retrievedModel) {
return;
}
$rememberToken = $retrievedModel->getRememberToken();
return $rememberToken && hash_equals($rememberToken, $token) ? $retrievedModel : null;
}
ID を基に DB から user 情報を取得し、DB に登録されたそのユーザーのトークンと cookie から取り出した token が同じであれば、取得したユーザー情報を返すようです。
結論
Remember me のチェックを ON にした場合、ログイン時にトークンを生成する。
そして、ID・生成したトークン・パスワードを cookie に保存する。
さらに生成したトークンを users テーブルに登録しておく。
そして、ユーザー情報が null になった場合(つまりログアウト状態)を auth ミドルウェアなどを通して検知した際に、cookie に保存していた id とトークンを利用して、ユーザー情報を取得し、自動でログインしたように見せる。
以上です。
おわりに
さきに Middleware を解読したことによって、この辺りの処理を解読していくのはかわいいものでした。
そして次回ですが、アカウント生成とメール認証について見ていこうかと考えています。
メンバー募集中!
サーバーサイド Kotlin コミュニティを作りました!
Kotlin ユーザーはぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion