🤩

Laravel11 マルチ認証環境でのセッション管理をカスタマイズ

に公開

はじめに

Laravelでマルチ認証(User認証&Admin認証など)を実装する際、デフォルトのセッション管理では問題が発生します。具体的には、AdminとUserの両方が同じuser_idカラムを使用するため、Admin認証時にもuser_idにAdminのIDが格納されてしまいます。

この記事では、この問題を解決するためのカスタムセッションハンドラーの実装方法を紹介します。

前提知識:Laravelのセッション設定

セッション設定ファイル

Laravelのセッション設定はconfig/session.phpに保存されます。デフォルトではdatabaseドライバーが使用されるようになっています。

利用可能なセッションドライバー:

  • file - ファイルベース(開発・小規模向け)
  • cookie - クッキーベース
  • database - データベース(本番環境推奨)
  • redis - Redis(本番環境推奨)
  • その他:apc, memcached, dynamodb, array

デフォルトのsessionsテーブル構造

Laravelインストール時に作成されるsessionsテーブルは以下の構造になっています:

Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->foreignId('user_id')->nullable()->index();
    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->longText('payload');
    $table->integer('last_activity')->index();
});

ご覧の通り、user_idカラムしか存在しないため、AdminとUserを区別できません。

解決策:カスタムセッションハンドラーの実装

Step 1: admin_idカラムの追加

まず、sessionsテーブルにadmin_idカラムを追加します。

php artisan make:migration add_admin_id_to_sessions_table --table=sessions

マイグレーションファイルの内容:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('sessions', function (Blueprint $table) {
            $table->unsignedBigInteger('admin_id')->nullable()->after('user_id')->index();
        });
    }

    public function down(): void
    {
        Schema::table('sessions', function (Blueprint $table) {
            $table->dropColumn('admin_id');
        });
    }
};
php artisan migrate

Step 2: カスタムセッションハンドラーの作成

app/Http/Sessionsディレクトリを作成し、CustomDatabaseSessionHandler.phpを作成します:

<?php

namespace App\Http\Sessions;

use Illuminate\Session\DatabaseSessionHandler;
use Illuminate\Contracts\Auth\Factory as AuthFactory;

class CustomDatabaseSessionHandler extends DatabaseSessionHandler
{
    protected AuthFactory $auth;

    public function __construct($connection, $table, $lifetime, $container, AuthFactory $auth)
    {
        parent::__construct($connection, $table, $lifetime, $container);
        $this->auth = $auth;
    }

    /**
     * セッションのデフォルトペイロードを取得
     */
    protected function getDefaultPayload($data): array
    {
        $payload = parent::getDefaultPayload($data);
        
        $this->setAuthenticationData($payload);
        
        return $payload;
    }

    /**
     * セッションIDに対する挿入処理を実行
     */
    protected function performInsert($sessionId, $payload): bool
    {
        $this->setAuthenticationData($payload);
        
        return parent::performInsert($sessionId, $payload);
    }

    /**
     * セッションIDに対する更新処理を実行
     */
    protected function performUpdate($sessionId, $payload): int
    {
        $this->setAuthenticationData($payload);
        
        return parent::performUpdate($sessionId, $payload);
    }

    /**
     * 認証情報をペイロードに設定
     */
    protected function setAuthenticationData(array &$payload): void
    {
        if ($this->auth->guard('admin')->check()) {
            // Admin認証済み
            $payload['admin_id'] = $this->auth->guard('admin')->id();
            $payload['user_id'] = null;
        } elseif ($this->auth->guard('web')->check()) {
            // User認証済み
            $payload['user_id'] = $this->auth->guard('web')->id();
            $payload['admin_id'] = null;
        } else {
            // 未認証
            $payload['user_id'] = null;
            $payload['admin_id'] = null;
        }
    }
}

Step 3: AppServiceProviderでの登録

app/Providers/AppServiceProvider.phpbootメソッドにカスタムセッションハンドラーを登録します:

<?php

namespace App\Providers;

use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Session\SessionManager;
use Illuminate\Auth\Middleware\RedirectIfAuthenticated;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use App\Http\Sessions\CustomDatabaseSessionHandler;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // カスタムセッションハンドラーの登録
        $this->app->extend('session', function (SessionManager $manager) {
            $manager->extend('database', function ($app) {
                $config = $app['config']['session'];
                $connection = $app['db']->connection($config['connection']);
                $auth = $app->make(AuthFactory::class);

                return new CustomDatabaseSessionHandler(
                    $connection,
                    $config['table'],
                    $config['lifetime'],
                    $app,
                    $auth
                );
            });

            return $manager;
        });

        // 認証後のリダイレクト処理
        RedirectIfAuthenticated::redirectUsing(function (Request $request) {
            if ($request->routeIs('admin.*') || Auth::guard('admin')->check()) {
                return route('admin.dashboard');
            }
            
            return route('dashboard');
        });
    }
}

動作確認

実装完了後、以下の動作を確認できます:

  1. Admin認証時: sessionsテーブルのadmin_idに値が設定され、user_idnull
  2. User認証時: sessionsテーブルのuser_idに値が設定され、admin_idnull
  3. 未認証時: 両方ともnull

注意点とベストプラクティス

明示的なnull設定の重要性

コード内でnullを明示的に設定している理由は、値が設定されていない場合にLaravelが予期しない動作をすることがあるためです。特にセッション管理では確実性が重要です。

パフォーマンスの考慮

このカスタマイズにより、セッション更新のたびに認証状態をチェックするオーバーヘッドが発生します。高負荷なアプリケーションでは、Redisセッションドライバーの使用を検討することをお勧めします。

セキュリティの考慮

セッションテーブルに認証情報を直接保存することで、セッション管理がより透明になりますが、同時にセッションデータの保護がより重要になります。適切なデータベースアクセス制御を確実に実装してください。

まとめ

Laravelのマルチ認証環境でのセッション管理問題を、カスタムセッションハンドラーを使用して解決する方法を紹介しました。この実装により、AdminとUserのセッションが適切に分離され、認証状態の混同を防ぐことができます。

より複雑な要件がある場合は、セッション管理ライブラリの導入や、マイクロサービス化も検討してみてください。

Discussion