41
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

Organization

Laravel8+laravel/breezeでマルチログイン

この記事はアクシス Advent Calendar 2020 1日目です。
株式会社アクシスの渋谷オフィスdev-campの成果を基に作成しています。

はじめに

Laravel8では、今までのlaravel/uiに代わる公式の認証パッケージとしてJetstreamが導入されました。
laravel/uiが使えなくなったわけじゃないですが、Jetstreamの方が公式の推しっぽい。認証のドキュメントで紹介されてるのもJetstreamですし

とはいえこのJetstream、とりあえずで使うにはあまりにも全部盛りが過ぎるためか、Laravelの中の人が新たに認証パッケージを開発していて、それがlaravel/breezeです。

ただlaravel/breezeはデフォルトではUserのログインしかできないので、AdminとUserの2種類でログインできるようにしてみよう、というのがこの記事の主旨です。
※ ひとまず2種類のユーザーでログインできるところまでで、メール認証やパスワード再設定とかまでは手が回りませんでした。

laravel/breezeについてもう少し補足

  • vueやreact不使用。controllerとbladeだけ使って、普通のwebアプリとして動きます。私のようなフロントエンドよくわからんおじさんでも安心。
  • ただしtailwind cssを使うのでnodeは必要です。laravel-mixが介護してくれるので頑張りましょう。
  • controllerやbladeは全部プロジェクト内のappresourcesにコピーされるので、カスタマイズ自由。vendorの中にコードがあって手出しできない…みたいなことにはなりません。
  • Jetstream(気流), tailwind(追い風), breeze(そよ風)となんか風関連のネーミングでまとめてますね。Laravel界隈はそういう機運なのか。

構成

環境構築は大体docker-composeを使いますが、nodeだけはasdfを使ってホストPCにローカルインストール。

  • macOS Catalina 10.15.7
  • node v14.14.0
  • docker desktop 2.5.0.1
    • php:7.4-fpm-alpine
    • nginx:1.18.0
    • composer:2
    • redis:5.0.7 セッションに使うだけです
    • mailhog:1.0.1 smtpサーバとして使うつもりでしたが今回は不要というか、そこまでできなかった…
  • Laravel 8.16.1
  • laravel/breeze 0.1.0

セットアップ

Laravel本体のインストール


$ composer create-project --prefer-dist laravel/laravel [何か適当にプロジェクト名]

laravel/breezeインストール

リポジトリのREADMEに従って

$ composer composer require laravel/breeze --dev
$ php artisan breeze:install

ここまででプロジェクト内にディレクトリが作られ、controllerやresources, routeなどのファイルが自動生成されます。こんな感じの構成に。
スクリーンショット 2020-11-30 2.15.31.png
スクリーンショット 2020-11-30 2.15.49.png

breeze:install完了後は以下のメッセージが出るので

Breeze scaffolding installed successfully.
Please execute the "npm install && npm run dev" command to build your assets.

言われたとおりにnpm実行。
laravel/breezelaravel-mixが同梱されてるので、コマンド叩くだけで動きます。

$ npm install && npm run dev

完了したらhttp://localhost:8231を開いてみましょう。
スクリーンショット 2020-11-30 2.33.16.png

ページ右上にLogin, registerが出てきましたね。
HelloWorld的なやつはここで終わりです。
以降はカスタマイズしてマルチログインを実装していきます。

Adminユーザー作成

管理画面にログインするユーザーを想定したAdminモデルを作成します。-mでマイグレーションも一緒に。

$ php artisan make:model AdminModel -m

モデルクラスに認証機能を持たせるため、継承元を変更します。

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Admin extends Authenticatable # ModelからAuthenticatableに変更
{
    use HasFactory, Notifiable;
    protected $guard = 'admins';
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    protected $hidden = [
        'password',
        'remember_token',
    ];
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

続いてマイグレーション作成。usersテーブルのマイグレーションファイルを丸パクリです。

    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
            $table->softDeletes();
        });
    }

マイグレーションを実行します。

$ app php artisan migrate

これでAdminユーザーを扱うテーブルとモデルクラスが出来上がりました。
続いてルート設定です。

ルート設定

routes/に設定ファイル作成

前述のbreeze:installでルート設定ファイルも設置されています。
routes/auth.phpを見てみましょう。結構な行数があるのでコードの貼り付けはしません。カスタマイズ前のコードを見たい方はリポジトリのコードをどうぞ

やはりルート設定もUserだけがログインする想定ですので、Adminのルートを追加します。
routes/web.php, routes/auth.phpは削除し、routes/user.php, routes/admin.phpを作成しています。
実際のコードは↓をご覧ください。web.phpとauth.phpをマージしたような内容です。
なおprefixはここでは設定しません。次項で説明します。

ルートプロバイダ

app/Providers/RouteServiceProvider.phpを編集します。
laravel/breezeが設定しているHOMEがユーザー向けのリダイレクト先なので、AdminユーザーのホームURLとなる定数ADMIN_HOMEを設定します。

    public const HOME = '/dashboard';
    public const ADMIN_HOME = '/admin/dashboard'; // 追記

続いてboot()メソッドにUserとAdminのルートを設定します。
example.com/でUser、example.com/adminはAdminユーザーがアクセスする領域を想定し、prefixを設定します。
またas()で名前をつけることでルート名にもprefixがつき、route()の引数を見るだけで何のルートを出力するのか分かりやすくします。

    public function boot()
    {
        $this->configureRateLimiting();

        $this->routes(function () {
            Route::prefix('api')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));

            Route::prefix('/')
                ->as('user.')
                ->middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/user.php'));

            Route::prefix('admin')
                ->as('admin.')
                ->middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/admin.php'));
        });
    }

これでルート設定は完了しました。
次はGuard設定です。

Guard設定

Laravel標準の認証用機能であるGuardを使用します。
Guardについての説明はそれだけで1本の記事になってしまうので、参考リンクの紹介にとどめます。
ただ、今回のカスタマイズで「なぜそうするのか」の理解につながるので、一読をおすすめします。

ではconfig/auth.phpを編集しましょう。変更箇所だけ抜粋します。

    'defaults' => [
        'guard' => 'users', // ここ変更
        'passwords' => 'users',
    ],
    'guards' => [
        // 追記ここから
        'users' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'admins' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
        // ここまで

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        // adminsを追加
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
    ],
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        // adminsを追加
        'admins' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

Guard設定追加完了。まだまだ続きます。

Middleware設定

Authenticate

アクセスしたユーザーが未認証の場合のリダイレクト処理を書きます。全体的に変えてます。

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Route;

class Authenticate extends Middleware
{
    protected string $user_route  = 'user.login';
    protected string $admin_route = 'admin.login';

    protected function redirectTo($request)
    {
        if (!$request->expectsJson()) {
            if (Route::is('user.*')) {
                return route($this->user_route);
            } elseif (Route::is('admin.*')) {
                return route($this->admin_route);
            }
        }
    }
}

RedirectIfAuthenticated

こちらは「ログイン済みのユーザーがアクセスしてきたらリダイレクトする」処理を書きます。
例えばログイン済みユーザーがログインフォームにアクセスした時とかですね。
リダイレクト先のURLは先述のルートプロバイダで定義したものです。

class RedirectIfAuthenticated
{
    private const GUARD_USER = 'users';
    private const GUARD_ADMIN = 'admins';

    public function handle(Request $request, Closure $next, ...$guards)
    {
        if (Auth::guard(self::GUARD_USER)->check() && $request->routeIs('user.*')) {
            return redirect(RouteServiceProvider::HOME);
        }
        if (Auth::guard(self::GUARD_ADMIN)->check() && $request->routeIs('admin.*')) {
            return redirect(RouteServiceProvider::ADMIN_HOME);
        }

        return $next($request);
    }
}

ミドルウェアはこの2つのみで、次はリクエストクラスです。
だいぶ長いですね、ダレてきそうです。もうちょっとお付き合いください。

リクエストクラス

ログインフォームで入力した値を処理するapp/Http/Requests/Auth/LoginRequest.phpを編集します。
Admin用とUser用のどちらのフォームからログインしてきたかを判別して、Auth::guard()に渡しています。

    public function authenticate()
    {
        $this->ensureIsNotRateLimited();

        // 追加ここから
        if ($this->routeIs('admin.*')) {
            $guard = 'admins';
        } else {
            $guard = 'users';
        }

        if (! Auth::guard($guard)->attempt($this->only('email', 'password'), $this->filled('remember'))) {
        // 追加ここまで
            RateLimiter::hit($this->throttleKey());

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

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

もうちょっとだけ続くんじゃ。

Admin用のcontrollerやbladeを作成する

ファイル複製

UserとAdminとでディレクトリを分けてcontrollerを作成します。bladeも同様。
breezeがインストールしたファイルをがさっとコピーして、User用とAdmin用とに分けます。

  • controller
    • app/Http/Controllers/Authapp/Http/Controllers/User/Authにディレクトリごと移動
    • app/Http/Controllers/User/Auth をコピーして app/Http/Controllers/Admin/Authにディレクトリごとリネーム
  • view
    • resources/views/authresources/views/user/authにディレクトリごと移動
    • resources/views/user/auth をコピーして resources/views/admin/authにディレクトリごとリネーム

コード修正

元々はroute()の引数がloginとかregisterみたいになってますが、ルート設定の作業を経てadmin.loginuser.dashboardといったルート名になってますので、それに合わせて修正していきます。
ルート一覧の確認はおなじみのphp artisan route:listで。| grep "admin."とかやれば、adminだけのルート一覧表示とかできます。

ログイン/ログアウト処理も少し手を入れます。
たとえばAdminのログインはApp\Http\Controllers\User\Auth\AuthenticatedSessionController::store()ですが、リダイレクト先がRouteServiceProvider::HOMEになってるので、これをRouteServiceProvider::ADMIN_HOMEに直したり。
一方でログアウト処理は同クラスのdestroy()ですが、ここで呼び出してるAuth::logout()も、まずguardを指定してからログアウトします。
指定しないと「AdminでログアウトしたのにUserの方でログアウトした」みたいなことになります。
具体的には以下のようなコードに。

    public function destroy(Request $request)
    {
        // guardを指定してログアウト
        Auth::guard('admins')->logout();

        // invalidate()でセッションデータ全体が破棄されてしまうのでコメントアウト
        //$request->session()->invalidate();

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

        return redirect('/');
    }

パスワードリセットとかメールアドレス確認の処理には今回手を入れてないんですが、guardの指定はたぶんAuth::***()的なコード全部が対象になるんじゃないでしょうか。まずAuth::guard('admins')してから->validate()とか->login()とか呼び出す感じで。

で、出来上がったものが以下になります。これも結構な量になるのでgithubをご覧ください。
viewはcomponentsやlayoutなどの共通パーツはそのままで、各ページのメインコンテンツのbladeを分けた感じ。

作業は以上です。おつかれさまでした。
このカスタマイズで、ひとまずログイン/ログアウト部分はマルチユーザーに対応できました。

さいごに

laravel/breezeは本当に最低限のスキャフォールドしかせず、あとはお好きなようにって感じですね。
guardとかの理解さえすれば、柔軟に対応できていいんじゃないでしょうか。そもそもLaravel標準の認証はguardだし。
私も今回の記事を書くにあたって、今まで何となく使ってたAuth::user()が何してるのかとか理解できてよかったです。
あと、controllerとbladeで全部解決してるので、vueやreactがよく分からない古のwebアプリおじさんにも安心。

ところで私はPHP開発にPHPStormを使ってますが、今回laravel ideaをお試し導入しました。ルート名やviewファイルの入力補完とかしてくれて、地味に便利。30日トライアルだけど買っちゃうか…

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
Sign upLogin
41
Help us understand the problem. What are the problem?