2024-06-19

LaravelのSession/CSRFの仕組み

Laravelのsession/csrfまわりのハンドリング何しているかのコードリーディングメモ。

Session

Illuminate\Session\Middleware\StartSession ミドルウェアでセッションのハンドリングをしています。

public function handle($request, Closure $next)
{
    if (! $this->sessionConfigured()) {
        return $next($request);
    }

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

    if ($this->manager->shouldBlock() ||
        ($request->route() instanceof Route && $request->route()->locksFor())) {
        return $this->handleRequestWhileBlocking($request, $session, $next);
    }

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

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L51-L65

getSession() ではクッキーからセッションIDを取得し、 Illuminate\Session\Store::setId() で取得したセッションIDをセットします。

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

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L157-L162

setId() は指定したIDを設定するか、新しくセッションIDを発行します。

public function setId($id)
{
    $this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L649-L652

セッションIDを設定したあとは、 handleStatefulRequest() を呼び出します。

protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
    $request->setLaravelSession(
        $this->startSession($request, $session)
    );
    
    // ...(省略)...

    $response = $next($request);

    $this->storeCurrentUrl($request, $session);

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

    $this->saveSession($request);

    return $response;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L110-L133

setLaravelSession() では $request->session() などのリクエストクラスからのsession呼び出しができるようにしています。

startSession() では Store::start() => Store::loadSession() という感じで呼び出していて、セッションIDを使ってセッションストレージからセッションを取り出して $attributes プロパティにセットしています。

public function start()
{
    $this->loadSession();

    if (! $this->has('_token')) {
        $this->regenerateToken();
    }

    return $this->started = true;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L142-L149 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L83-L92

またCSRFトークンである _token がセッション内に存在しない場合は再生成しています。

public function regenerateToken()
{
    $this->put('_token', Str::random(40));
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Store.php#L703-L706

$next($request) で本処理が完了したら、storeCurrentUrl() でセッションの _previous.url にリクエストURLをセットします。

protected function storeCurrentUrl(Request $request, $session)
{
    if ($request->isMethod('GET') &&
        $request->route() instanceof Route &&
        ! $request->ajax() &&
        ! $request->prefetch() &&
        ! $request->isPrecognitive()) {
        $session->setPreviousUrl($request->fullUrl());
    }
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L200-L209

addCookieToResponse() ではSet-CookieでセッションIDをセットするようにレスポンスヘッダの操作をしています。

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,
            $config['partitioned'] ?? false
        ));
    }
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L218-L234

最後に saveSession() でセッションをセッションストレージに保存します。

protected function saveSession($request)
{
    if (! $request->isPrecognitive()) {
        $this->manager->driver()->save();
    }
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Session/Middleware/StartSession.php#L242-L247

CSRF

Illuminate\Foundation\Http\Middleware\VerifyCsrfToken で処理をしています。

public function handle($request, Closure $next)
{
    if (
        $this->isReading($request) ||
        $this->runningUnitTests() ||
        $this->inExceptArray($request) ||
        $this->tokensMatch($request)
    ) {
        return tap($next($request), function ($response) use ($request) {
            if ($this->shouldAddXsrfTokenCookie()) {
                $this->addCookieToResponse($request, $response);
            }
        });
    }

    throw new TokenMismatchException('CSRF token mismatch.');
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php#L73-L89

となっている。 tokensMatch()getTokenFromRequest() で取ってきたトークンがセッションに含まれるトークン(上述の _token )と合致しているかをチェックしています。

protected function tokensMatch($request)
{
    $token = $this->getTokenFromRequest($request);

    return is_string($request->session()->token()) &&
           is_string($token) &&
           hash_equals($request->session()->token(), $token);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php#L128-L135

getTokenFromRequest() はformデータやHTTPヘッダからCSRFトークンを取得してます。

protected function getTokenFromRequest($request)
{
    $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');

    if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
        try {
            $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
        } catch (DecryptException) {
            $token = '';
        }
    }

    return $token;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php#L143-L156

Bladeの csrf_field() はヘルパーで定義されており、input hiddenを作成しています。

function csrf_field()
{
    return new HtmlString('<input type="hidden" name="_token" value="'.csrf_token().'" autocomplete="off">');
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/helpers.php#L324-L327

csrf_token() はセッションからトークンデータを取り出しています。

function csrf_token()
{
    $session = app('session');

    if (isset($session)) {
        return $session->token();
    }

    throw new RuntimeException('Application session store not set.');
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/helpers.php#L338-L347

このエントリーをはてなブックマークに追加