🔑

laravelのセッションによるログイン判定を本気で理解する(Part2.ログイン編)

2023/01/17に公開

はじめに

前回はサイトを閲覧したときセッションが作成される仕組み、Set-CookieでブラウザにセッションIDがセットされるところを見ました。

今回は作成されたセッションにログインされるとuser_idがセットされるところを見ましょう。

前回

https://zenn.dev/cube/articles/90d16cc53453d1

ログイン

ログイン時の挙動なので、AuthenticatedSessionController->storeから見ていきます。

Route::post('login', [AuthenticatedSessionController::class, 'store']);
 AuthenticatedSessionController.php
 public function store(LoginRequest $request): RedirectResponse
    {
        $request->authenticate();

        $request->session()->regenerate();

        return redirect()->intended(RouteServiceProvider::HOME);
    }
 
  LoginRequest.php
  public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

ログイン処理はAuth::attemptにありそうですね。
Auth::attemptから定義先に飛ぶとIlluminate\Support\Facades\Auth.phpに行きます。
ですがAuth.phpにも、継承元のFacade.phpにはもattempt()がありません。
ここからどう探せばいいのでしょうか。

phpで読んでてメソッドが見つからなくなったら、僕は下記の二つの方法で探しています。
1.そのクラスや継承元のクラスで_call、_call_staticを探す
2.検索でそのメソッドを調べて、実際どこで読んでいそうなのか当たりを付ける

今回は1でやってみましょう。

マジックメソッド_call,_call_staticについて
もしメソッドがクラス内に無かったら呼ばれるメソッドです。
今回Auth::attemptでstaticなので_call_staticを見ます。

Illuminate\Support\Facades\Facade.php
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(...args)で解決してそうな雰囲気があります。
なので$instanceに正しいattemptをもっているクラスが入っていそうですね。
getFacadeRoot以下を追っていきます。

Illuminate\Support\Facades\Facade.php
public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

static::getFacadeAccessorはFacade.phpではなくAuth.phpでオーバーライドされているのでAuth.phpの方を見ます。

 Illuminate\Support\Facades\Auth.php
 protected static function getFacadeAccessor()
    {
        return 'auth';
    }
 Illuminate\Support\Facades\Facade.php
 ここでは$name="auth"
 
 protected static function resolveFacadeInstance($name)
    {
        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            if (static::$cached) {
                return static::$resolvedInstance[$name] = static::$app[$name];
            }

            return static::$app[$name];
        }
    }

上記のresolvedInstanceはキャッシュっぽいのでstatic::app[name]からとってくるのが本命っぽいです。
ここで$appはlaravelが持つ機能であるサービスコンテナのことです。
サービスコンテナについては深く立ち入らないとして、とりあえずサービスコンテナが何を返してくれるか見るために、config/app.phpのprovidersを見てみます。
今回はauthを見たいのでAuthServiceProviderを見ると、authにはどうやらAuthManagerが紐づいていることがわかります。

protected function registerAuthenticator()
    {
        $this->app->singleton('auth', fn ($app) => new AuthManager($app));

        $this->app->singleton('auth.driver', fn ($app) => $app['auth']->guard());
    }

サービスコンテナについては下記で。
https://zenn.dev/cube/articles/f88c5b6654e729

ここで_call_staticに戻ると

Illuminate\Support\Facades\Facade.php

$method=attempt
$args=["email"=> ...,"password" => ...]
public static function __callStatic($method, $args)
    {
        return authManger->attempt(["email"=> ...,"password" => ...]);
    }

となっていることがわかります。
ここからはAuthManagerを見ていきましょう。

AuthManager

さてattemptを見ていく流れなのですが、AuthManagerクラスにattemptが存在しません。
今度はstaticではないので_callを見ると、下記のようになってます。

   
   Illuminate\Auth\AuthManager.php
   public function __call($method, $parameters)
    {
        return $this->guard()->{$method}(...$parameters);
    }

どうやら悲しいことにattemptにはまだたどり着けそうもありません...
仕方がないのでguardを見てきます。


  Illuminate\Auth\AuthManager.php
  public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }
   
   Illuminate\Auth\AuthManager.php
   public function getDefaultDriver()
    {
        return $this->app['config']['auth.defaults.guard'];
    }

ここで、$this->app['config']['auth.defaults.guard']と出てきました。
サービスコンテナとconfig,arrayAccessの関係,itemsがどこでセットされているかについては長くなってしまうので下記で。
https://zenn.dev/cube/articles/d7b978c5eabde5#configの本体

ここではapp["config"]は\Illuminate\Config\Repository::classを、
Repository->$itemsは下記のようになっていると答えだけ示します。
$items["auth"] = [
...
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
...
]

app['config'] = \Illuminate\Config\Repository::classで
Repository['auth.defaults.guard']とクラスに配列アクセスしているのはRepositoryがArrayAccessを実装しているからです。
Repository['auth.defaults.guard']とするとRepository->offsetGet('auth.defaults.guard')が走ります。

Illuminate\Config\Repository.php
protected $items = [];

public function offsetGet($key): mixed
    {
        return $this->get($key);
    }
public function get($key, $default = null)
    {
        if (is_array($key)) {
            return $this->getMany($key);
        }

        return Arr::get($this->items, $key, $default);
    }

Arr::getはlaravelが用意したヘルパーで、ドットを使ってネストした配列にアクセスできるようになります。

public static function get($array, $key, $default = null)
    {
        ...

        foreach (explode('.', $key) as $segment) {
            if (static::accessible($array) && static::exists($array, $segment)) {
                $array = $array[$segment];
            } else {
                return value($default);
            }
        }

        return $array;
    }

例えば、$this->items['auth.defaults.guard']だったら、
$this->items["auth"]["dafaults"]["guard"]と読み替えられます。

$this->items['auth.defaults.guard'] = "web"となっていますね。

config/auth.php
'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

AuthManagerのguardに戻ると結局構造は下記のようになっています。

  Illuminate\Auth\AuthManager.php
  public function guard($name = null)
    {
        $name = "web";

        return $this->resolve($name);
    }

$nameがわかったところで次はresolveを見てみましょう。

 Illuminate\Auth\AuthManager.php

 $name = "web"
 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."
        );
    }

ざっと見るとcreate???Driverというメソッドを最終的に呼ぶことになりそうですね。
customCreatorsの部分は自作Driverを使った時なのでLaravel標準のDriverを使う今回は飛ばします。

customCreatorと自作ドライバの関係は下記がわかりやすいです。
https://www.1x1.jp/blog/2014/02/how-to-implement-custom-auth-driver-in-laravel.html

まずは???の部分がどうなるかを見てきましょう。

Illuminate\Auth\AuthManager.php

$name = "web"
protected function getConfig($name)
    {
        return $this->app['config']["auth.guards.{$name}"];
    }

これは以前やったのと同じく、config/auth.phpのguards.webを見ます。

config/auth.php
   'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

なのでresolveは以下の通り読み替えられます。

 Illuminate\Auth\AuthManager.php

 $name = "web"
 protected function resolve($name)
    {
        $config =  [
            'driver' => 'session',
            'provider' => 'users',
        ];

        
        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';
        
	// $driverMethod = createSessionDriver
	
        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }
    }

なので、AuthManager->createSessionDriver("web", [
'driver' => 'session',
'provider' => 'users',
])
を見ることになります。


Illuminate\Auth\AuthManager.php

$name = "web"
$config =  ['driver' => 'session',
            'provider' => 'users',]

public function createSessionDriver($name, $config)
    {
        $provider = $this->createUserProvider($config['provider'] ?? null);

        $guard = new SessionGuard(
            $name,
            $provider,
            $this->app['session.store'],
        );

        ...

        return $guard;
    }

createUserProviderと$this->app["session.store"]を見ていきましょう。

まずはcreateUserProviderから。
createUserProviderはAuthManagerが使用しているTrait、CreatesUserProvidersの中にあります。

Illuminate\Auth\CreatesUserProviders.php

$provider = "users"
 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."
            ),
        };
    }

customProviderCreatorsは今回飛ばします。


Illuminate\Auth\CreatesUserProviders.php
$provider = "users"
protected function getProviderConfiguration($provider)
   {
       if ($provider = $provider ?: $this->getDefaultUserProvider()) {
           return $this->app['config']['auth.providers.'.$provider];
       }
   }

config/auth.phpを参照して、providers.usersを探すと下記です。

    config/auth.php
    
    'providers' => [
       'users' => [
           'driver' => 'eloquent',
           'model' => App\Models\User::class,
       ],

       // 'users' => [
       //     'driver' => 'database',
       //     'table' => 'users',
       // ],
   ],

createUserProviderを読み替えると下記になります。

Illuminate\Auth\CreatesUserProviders.php

$provider = "users"
 public function createUserProvider($provider = null)
    {

        return match ("eloquent") {
            'database' => $this->createDatabaseProvider($config),
            'eloquent' => $this->createEloquentProvider($config),
            default => throw new InvalidArgumentException(
                "Authentication user provider [{$driver}] is not defined."
            ),
        };
    }

次に、$this->app["session.store"]を見ましょう。
サービスコンテナに"session.storeが登録されているのは、SessionServiceProviderです。

 Illuminate\Session\SessionServiceProvider.php
 protected function registerSessionManager()
   {
       $this->app->singleton('session', function ($app) {
           return new SessionManager($app);
       });
   }
   protected function registerSessionDriver()
   {
       $this->app->singleton('session.store', function ($app) {
           return $app->make('session')->driver();
       });
   }

なので$app->make("session") = SessionManager,
$app->make("session.store") = SessionManager->driver()ということがわかります。
これはPart1でみたStoreのことですね。

ここでsingletonで登録しているのがわかります。
singletonで登録しているということは$app->makeで何回呼んでも同じインスタンスが返ることになります。
singletonについては下記。
https://zenn.dev/cube/articles/f88c5b6654e729#singleton%2Cbind

なのでPart1でみたStoreと同じStoreが呼ばれることが保証され、データの中身もPart1で作ったときのままなのでIdとかもセット済みです。

ここでcreateSessionDriverを読み替えると、

Illuminate\Auth\AuthManager.php

$name = "web"
$config =  ['driver' => 'session',
           'provider' => 'users',]

public function createSessionDriver($name, $config)
   {

       $guard = new SessionGuard(
           "web",
           new EloquentUserProvider(\Illuminate\Hashing\HashManager, App\Models\User::class);,
           \Illuminate\Session\Store,
       );

       ...

       return $guard;
   }

さて、ここまでで
AuthManager->guard()->attempt()のうちAuthManager->guard()を読んできました。

AuthManager->guard():
   AuthManger->resolve():
     AuthManager->createSessionDriver():
        return SessionGuard

という構造になっているのでAuthManager->guard()はSessionGuardを返します。

なので、SessionGuard->attempt()となりますね。SessionGuardにこそattemptがあればいいのですが...

SessionGuardを見るとattemptが無事ありました!ようやくたどり着きましたね。


Illuminate\Auth\SessionGuard.php

 $this->provider =  new EloquentUserProvider(\Illuminate\Hashing\HashManager, App\Models\User::class)
 $credentials = ["email"=> ...,"password" => ...]
 
 
 public function attempt(array $credentials = [], $remember = false)
    {
       ...

       $user = $this->provider->retrieveByCredentials($credentials);

       if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

        ...
        return false;
    }

さてようやくログイン処理ですね。
EloquentUserProvider->retrieveByCredentialsではEloquentを使ってDBにアクセスしてます。
入力したemailとpasswordのユーザーがいるか確認ですね。
OKなら$this->loginにUserモデルが渡されます。

 public function login(AuthenticatableContract $user, $remember = false)
    {
        $this->updateSession($user->getAuthIdentifier());

        ...
    }

$user->getAuthIdentifier()は下記のようになっています。

App\Models\User extends Illuminate\Foundation\Auth\User 
となっていて、Auth\Userが使っているTrait Authenticatableにあります。

trait Authenticatable
{
   public function getAuthIdentifierName()
   {
       return $this->getKeyName();
   }

   public function getAuthIdentifier()
   {
       return $this->{$this->getAuthIdentifierName()};
   }

getKeyNameはIlluminate\Foundation\Auth\Userが継承しているIlluminate\Database\Eloquent\Modelにあります。

Illuminate\Database\Eloquent\Model
public function getKeyName()
   {
       return $this->primaryKey;
   }

user->getAuthIdentifier()は$this->idと読み替えられますね。

updateSessionを見ましょう。

Illuminate\Auth\SessionGuard.php
   
   $this->session = \Illuminate\Session\Store
   
   protected function updateSession($id)
   {
       $this->session->put($this->getName(), $id);

       $this->session->migrate(true);
   }
   
   $this->nane = "web"
   
   public function getName()
   {
       return 'login_'.$this->name.'_'.sha1(static::class);
   }

   \Illuminate\Session\Store.php

   $key = login_web_???? (???はsha1でハッシュされたもの)
   $value = user_id(1とか)
   
   public function put($key, $value = null)
   {
       if (! is_array($key)) {
           $key = [$key => $value];
       }

       foreach ($key as $arrayKey => $arrayValue) {
           Arr::set($this->attributes, $arrayKey, $arrayValue);
       }
   }

なのでupdateSessionでようやくSessionにuser_idがセットされることがわかります。

おわりに

Part2はかなり分量が長めとなってしまいました。お付き合いいただきありがとうございます。
ただここでしっかりやった分Part3ではかなり楽できるので、Part3は短めになっています。

次回Part3ではどうやってセッションをつかってログインを判定しているかを見ましょう。

次回

https://zenn.dev/cube/articles/90eacd92130e6a

Discussion