Help us understand the problem. What is going on with this article?

Laravelで複数ブラウザでのログインを制限する

概要

Laravelでログイン認証を実装する際、複数のブラウザでのログインを制限したい(他のブラウザで新たにログインしたらそれまでログインしていたブラウザのセッションを破棄する)という要件があった際の対応を記します。

5.6以降と5.5以前で実装が異なるのでご注意ください。
以下の実装は、それぞれ6.6.2、5.5.4で動作を確認しています。

実装

基本的な認証が実装してあることを前提とします。
まっさらなLaravelプロジェクトであれば、
5系ならphp artisan make:auth
6系ならcomposer require laravel/ui --devphp artisan ui vue --auth
を実行してある状態です。
参考:Laravel 6.x 認証

Laravel5.6以降

Laravel5.6以降であれば、AuthファサードのlogoutOtherDevicesメソッドで簡単に実装することができます。

まずは、app/Http/Kernel.phpクラスのwebミドルウェアグループにある
\Illuminate\Session\Middleware\AuthenticateSession::class
のコメントを外します。

app/Http/Kernel.php
protected $middlewareGroups = [
        'web' => [
            // ...
            \Illuminate\Session\Middleware\AuthenticateSession::class, // コメントを外す
            // ...
        ],

次に、AuthenticatesUsersトレイトのauthenticatedメソッドをオーバーライドして、そこに記述をします。

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; //追加
use Illuminate\Support\Facades\Auth; //追加

class LoginController extends Controller
{
    // ...

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return \Illuminate\Http\Response
     */
    public function authenticated(Request $request, $user)
    {
        Auth::logoutOtherDevices($request->password);
        return redirect()->intended($this->redirectPath());
    }
}

これだけで実装できてしまいます。ドキュメント通りの実装ですね。

Laravel5.5以前

Laravel5.5以前では上記のlogoutOtherDevicesメソッドが実装されていないため、別の方法をとる必要があります。
もちろんlogoutOtherDevicesメソッドを自前で用意してもよいのですが、今回は別のやり方をご紹介できればと思います。

まずは、ユーザーに紐付くセッションIDを格納するためのカラム(session_id)をusersテーブルに作成します。
php artisan make:migration add_session_id_to_users_table --table=users
でマイグレーションファイルを作成したら、以下のようにカラムを追加するコードを記述します。

yyyy_mm_dd_xxxxxx_add_session_id_to_users_table.php
<?php

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

class AddSessionIdToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->text('session_id')->after('remember_token')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('session_id');
        });
    }
}

ファイルが作成できたら、php artisan migrateでマイグレーションを実行し、カラムが追加されていることを確認します。

次に、ユーザー認証後にセッションを更新する処理を実装します。
ログイン処理が実行される度に以前のセッションを破棄することで、別のブラウザでログインされた際に以前のブラウザの認証を解除するという仕組みです。

ここで、config/auth.phpのprovidersを確認してください。
デフォルトであれば、ユーザープロバイダのドライバとしてeloquentが指定されています。

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

ドライバとしてdatabaseを指定している場合は実装が異なるため注意してください。
(カスタムユーザープロバイダの場合はさらに別の実装になるかと思います)

LoginController.phpで、AuthenticatesUsersトレイトのsendLoginResponseメソッドをオーバーライドし、以下のように記述します。

まずはドライバがeloquentの場合。

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; //追加
use Illuminate\Support\Facades\Auth; //追加
use Illuminate\Support\Facades\Session; //追加

class LoginController extends Controller
{
    // ...

    /**
     * Send the response after the user was authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendLoginResponse(Request $request)
    {
        $request->session()->regenerate();

        // 以前のセッションを破棄
        $previous_session = Auth::user()->session_id;
        if ($previous_session) {
            Session::getHandler()->destroy($previous_session);
        }

        // 新しいセッションIDをDBへ挿入
        Auth::user()->session_id = Session::getId();
        Auth::user()->save();

        $this->clearLoginAttempts($request);

        return $this->authenticated($request, $this->guard()->user())
                ?: redirect()->intended($this->redirectPath());
    }
}

ドライバがdatabaseの場合はこちら。

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; //追加
use Illuminate\Support\Facades\Auth; //追加
use Illuminate\Support\Facades\Session; //追加
use Illuminate\Support\Facades\DB; //追加

class LoginController extends Controller
{
    // ...

    /**
     * Send the response after the user was authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendLoginResponse(Request $request)
    {
        $request->session()->regenerate();

        // 以前のセッションを破棄
        $previous_session = Auth::user()->session_id;
        if ($previous_session) {
            Session::getHandler()->destroy($previous_session);
        }

        // 新しいセッションIDをDBへ挿入
        $new_session = Session::getId();
        DB::table('users')
                    ->where('id', Auth::user()->id)
                    ->update(['session_id' => $new_session]);

        $this->clearLoginAttempts($request);

        return $this->authenticated($request, $this->guard()->user())
            ?: redirect()->intended($this->redirectPath());
    }
}

ドライバがdatabaseの場合、Auth::user()で取得できるのものはIlluminate\Auth\GenericUserクラスのオブジェクトです。
このオブジェクトからはsaveメソッドにアクセスできないため、愚直にクエリビルダでupdateをかけています。

実装は以上です。
LTSリリースである5.5を使用しているプロジェクトはまだまだあるのではと思い書いてみました。
参考になれば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away