🔑

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

2023/01/17に公開

はじめに

laravelでauth::checkだったり、middlewareのauthを付けるとどうやってログイン済みか判定しているのかと思いつつ、定期的にちょこちょこコード読んで忘れての繰り返しなので忘れないための記事です。
特にfacade周りSessionGuard周りすぐ忘れるのでそこらへんはちゃんと書いていきます。

Version:
laravel 9.19

大枠の理解から

詳細なコードを追う前に流れの理解から行きましょう。

1.セッション作成編
まずサイトにアクセスしたときにそのユーザーのセッションが作られ、Set-CookieでセッションIDがセットされてアクセスしたユーザーとセッションが紐づく

2.ログイン編
ログインしたときにセッションにログインしたユーザーのID(大体user_id)がセット

3.ログイン判定編
ログイン後はセッションにユーザーIDがセットされているかをチェック

以上をlaravelではどうやっているか見ていきます。

セッション作成

セッションはStartSessionというMiddlewareが発火して作成されます。
StartSessionが発火するのはweb.phpに書かれているルートにアクセスしたときです。
laravelのミドルウェアの設定を見てみると、下記のようにwebの欄にStartSessionが含まれていることがわかります。

App\Http\Kernel.php

protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

 Illuminate\Session\Middleware\StartSession.php
 public function __construct(SessionManager $manager, callable $cacheFactoryResolver = null)
    {
        $this->manager = $manager;
        $this->cacheFactoryResolver = $cacheFactoryResolver;
    }

StartSessionのコードを読み進めるときにthis->managerが出てきますが、どこでこの$manegerが入るかというと下記で触れたKernel.phpのRegisterProvidersでやっています。
https://zenn.dev/cube/articles/f88c5b6654e729#laravelのスタート

ここでは$this->manager=Illuminate\Session\SessionManeger.phpと答えだけ示しておきます。

Middlewareはhandle()によって役割を果たすのでStartSession->handleから見ましょう。

Illuminate\Session\Middleware\StartSession.php

public function handle($request, Closure $next)
    {
        ...

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

       ...

        return $this->handleStatefulRequest($request, $session, $next);
    }
    
 public function getSession(Request $request)
    {
        return tap($this->manager->driver(), function ($session) use ($request) {
            $session->setId($request->cookies->get($session->getName()));
        });
    }

getSessionから見ていくと返り値がtapですね。
tapは第一引数を返して、第二引数は第一引数の加工をする関数です。
つまり、this->manager->driver()=$sessionなので、driverこそsessionなのかもという予想で読んでいきます。

manager=SessionManeger.phpはIlluminate\Support\Manager.phpを継承しています。
driver()はManager.phpにあるので、そこのdriver()を見ましょう。

Illuminate\Support\Manager.php
$driver = null

public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();
        
	...
	
	 if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }
        return $this->drivers[$driver];
    }

$this->getDefaultDriver()はManagerにはなく、Managerを継承した子クラスに実装されています。
なので今回はSessionManegerということになります。

Illuminate\Session\SessionManager.php

public function getDefaultDriver()
    {
        return $this->config->get('session.driver');
    }

ここで$this->config->get('session.driver')とありますね。
これはapp()->make("config")->get("session.driver")と読み替えられます。
app(),configの関係は下記で。
https://zenn.dev/cube/articles/d7b978c5eabde5#configの本体

ここでは、$this->config->get('session.driver')はconfig/session.phpのdriver = "file"
と答えだけ示しておきます。

defaultDriver名がfileとわかったので、manager->createDriver("file")を見ます。

Illuminate\Support\Manager.php

$driver = "file"
 protected function createDriver($driver)
    {
          ...
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                return $this->$method();
            }
          ...
    }

$method = "createFileDriver"となりまずが、createFileDriverはManagerを継承した子クラスであるSessionManagerにあるので、SessionManager->createFileDriver()を実行しています。

Illuminate\Session\SessionManager.php

protected function createFileDriver()
    {
        return $this->createNativeDriver();
    }


protected function createNativeDriver()
    {
        $lifetime = $this->config->get('session.lifetime');

        return $this->buildSession(new FileSessionHandler(
            $this->container->make('files'), $this->config->get('session.files'), $lifetime
        ));
    }
    
protected function buildSession($handler)
    {
        return $this->config->get('session.encrypt')
                ? $this->buildEncryptedSession($handler)
                : new Store(
                    $this->config->get('session.cookie'),
                    $handler,
                    $id = null,
                    $this->config->get('session.serialization', 'php')
                );
    }
config/session.php

 'cookie' => env(
        'SESSION_COOKIE',
        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
    )
    
 //serializarionは何も弄ってないと記載されません。

new Store(
laravel_session,
FileSessionHandler,
null,
php
)
と読み替えられます。

createFileDriverでは最終的にStoreが返ることがわかりますね。
handlerはconfig/session.phpで指定したものに応じて変わります、
fileだったら上記のようにFileSessionHandler、
databaseだったらDatabaseSessionHandlerとなり、
Storeから各種Handlerで対応するところ(File,Database)に情報を置いたり、貰ってきたりしてます。
Repositoryパターンみたいな感じですね。
今回はFileですが保存場所がどこでもやることが変わらないのでSession操作はStoreを見ていけばやってることがわかります。

ここでStartSessionまで戻ると、下記のように読み替えられます。

Illuminate\Session\Middleware\StartSession.php

 public function getSession(Request $request)
    {
        return tap(Store,function ($session) use ($request) {
            $session->setId($request->cookies->get($session->getName()));
        });
    }

getSessionの第二引数部分を見てみましょう。
$session = new Store(
name = laravel_session,
handler = FileSessionHandler,
id = null,
serialization = php
)
request->cookies->get("laravel_session")ですが、$request->cookies->getは与えられた引数がcookieの中になければnullを返します。
サイトアクセス初回だと想定すると返ってくるのはnullです。

なのでsession->setId(null)と読んでいきます。

 public function setId($id)
    {
        $this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
    }
 public function isValidId($id)
    {
        return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
    }
  protected function generateSessionId()
    {
        return Str::random(40);
    }

ここでIdが生成されました。

セッションが作られたので、あとはSet-CookieでセッションIdがセットされているところを見ればPart1の目標を達成できますね。
では、もう一度StartSessionまで戻りましょう。

Illuminate\Session\Middleware\StartSession.php

public function handle($request, Closure $next)
    {
        ...

        $session = Store

       ...

        return $this->handleStatefulRequest($request, $session, $next);
    }
    
protected function handleStatefulRequest(Request $request, $session, Closure $next)
    {
       
       ....

        $this->addCookieToResponse($response, $session);

        ...

        return $response;
    }
    
protected function addCookieToResponse(Response $response, Session $session)
    {
        if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) {
            $response->headers->setCookie(new Cookie(
                $session->getName(), $session->getId(), $this->getCookieExpirationDate(),
                $config['path'], $config['domain'], $config['secure'] ?? false,
                $config['http_only'] ?? true, false, $config['same_site'] ?? null
            ));
        }
    }
    

addCookieToResponseでresponseにsessionを保存していることがわかりますね。
デフォルトでは、
$session->name = laravel_session
$session->id = Str::random(40)
となっていたので、これで初回アクセス後Cookieを見るとlaravel_sessionとセッションの情報がCookieにセットされているのがわかります。

おわりに

以上でPart1のセッションの情報がCookieにセットされるまでを見ました。
Part2ではセッションにユーザーIDがセットされるのはどんな仕組みなのかまでをやります。

次回

https://zenn.dev/cube/articles/7d509c01cc82a6

Discussion