Laravel 7.xでマルチ認証機能を作成してみた

加藤です。弊社にもリモートワークが導入されてからというもの、元々の出不精に一層の拍車がかかった気がしてなりません。(汗)

Laravel 7.xでマルチ認証機能を作成してみたので、備忘のために今回はそのことを記事にしようと思います。

環境

  • PHP 7.3
  • Laravel 7.0

目標・やりたいこと

一般ユーザ用と管理者用とでログイン画面を分ける (ユーザ登録やパスワードリセット機能等は本記事内では設定しません)

(1)認証に必要なviewとRoute、Cotrollerを作成

readouble.com

認証に必要な諸々を作成してくれるコマンドが用意されているのでそれを使います。

composer require laravel/ui --dev
php artisan ui vue --auth

(2)管理者ユーザ情報を管理するためのAdmin Modelとadminsテーブルのmigrationを作る

以下のコマンドでModelとmigrationを一緒に作ることができます。

php artisan make:model Models/Admin -m

Admin Modelとmigrationの中身はそれぞれ、User Modelとusersのmigrationと同じです。

app/Admin.php

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Admin extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

database/migrations/create_admins_table.php

<?php

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

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

    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

(3)migration実行

migrationを実行してadminsテーブルを作成します。

php artisan migrate

(4)auth.phpにAdminの定義を追加

config/auth.php

<?php 

'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'user' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [          // adminの定義を追加
            'driver' => 'session',
            'provider' => 'admins',
        ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
        'admins' => [         // adminの定義を追加
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],
    ],

'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

(5)RouteServiceProviderに管理者用のパスを追加

RouteServiceProviderに管理者用のパスとしてADMIN_HOMEを追加します。

app/Providers/RouteServiceProvider.php

<?php

class RouteServiceProvider extends ServiceProvider
{
    public const HOME = '/home';
    public const ADMIN_HOME = '/admin/home';    // 管理者用のパスを追加
}

(6)Authenticateに未ログイン時の挙動を書く

app/Http/Middleware/Authenticate.php

<?php

protected function redirectTo($request)
    {

        if (! $request->expectsJson()) {
            if (Route::is('admin.*')) {    // admin からのRouteの分岐を追加
                return route('admin.login');
            } else {
                return route('login');
            }
        }
    }

(7)RedirectIfAuthenticatedにログイン時の挙動を書く

app/Http/Middleware/RedirectIfAuthenticated.php

<?php

public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check() && $guard === 'user') {        // guardがuserかadminかで分岐させるよう変更
            return redirect(RouteServiceProvider::HOME);
        } elseif (Auth::guard($guard)->check() && $guard === 'admin') {
            return redirect(RouteServiceProvider::ADMIN_HOME);
        }

        return $next($request);
    }

(8)Controllerを追加

user用とAdmin用のControllerを追加します。

(8-1)HomeController

Admin用 app/Http/Controllers/Admin/HomeController.php

<?php

namespace App\Http\Controllers\Admin;   // namespaseに注意

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function index()
    {
        return view('admin.home');
    }
}

user用 app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

class HomeController extends Controller
{
    public function index()
    {
        return view('user.home');
    }
}

(8-2)LoginController

Admin用 app/Http/Controllers/Admin/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Admin\Auth;   // namespaseに注意

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = RouteServiceProvider::ADMIN_HOME;

    protected function guard()
    {
        return Auth::guard('admin');
    }

    public function showLoginForm()
    {
        return view('admin.auth.login');
    }

    public function logout(Request $request)
    {
        Auth::guard('admin')->logout();

        return redirect(route('admin.login'));
    }
}

user用 app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = RouteServiceProvider::HOME;

    // Guardの認証方法を指定
    protected function guard()
    {
        return Auth::guard('user');
    }

    // ログイン画面
    public function showLoginForm()
    {
        return view('user.auth.login');
    }

    // ログアウト処理
    public function logout(Request $request)
    {
        Auth::guard('user')->logout();

        return redirect(route('login'));
    }
}

(9)ルーティング設定

ユーザ用、管理者用それぞれのルーティング設定をします。

routes/web.php

<?php

use Illuminate\Support\Facades\Route;

// ユーザ用のルーティング
Auth::routes([
    'register' => false,
    'reset'    => false,
    'verify'   => false
]);

// ユーザのhome(認証後は/homeへ)
Route::middleware('auth:user')->group(function () {
    Route::get('/home', 'HomeController@index')->name('home');
});

// 管理者用のルーティング
Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function () {

    Auth::routes([
        'register' => false,
        'reset'    => false,
        'verify'   => false
    ]);
 // 管理者のhome(認証後は/admin/homeへ)
    Route::middleware('auth:admin')->group(function () {
        Route::get('home', 'HomeController@index')->name('home');
    });
});

(10)管理者用のviewを追加する

(1)の工程で作られたhome.blade.phpとauth/login.blade.phpをコピーしてuserとadminにそれぞれ同じような構成で作ります。 layoutsもユーザ用と管理者用で分けてみます。

view
|
|- admin
   |- auth
       |- login.blade.php
   |- home.blade.php
|
|- layouts
   |- admin
       |- app.blade.php
   |- user
       |- app.blade.php
|- user
   |-auth
       |- login.blade.php
   |- home.blade.php

viewの階層構造はこのような感じにしました。

(11)view内のパスを合わせる

layoutsのextendsやログインform内のパスを合うように変更します。

管理者用 resources/views/layouts/admin/app.blade.php

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">

                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @unless (Auth::guard('admin')->check())    // adminのguardのチェック
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
                            </li>
                            @if (Route::has('register'))
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('admin.register') }}">{{ __('Register') }}</a>   // ここのroute()を変える
                                </li>
                            @endif
                        @else
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    <span class="caret"></span>
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('admin.logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;">  // ここのroute()を変える
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        @endunless
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
</body>
</html>

resources/views/admin/auth/login.blade.php

@extends('layouts.admin.app')    // ここを変える

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.login') }}">    // ここのroute()を変える
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <div class="col-md-6 offset-md-4">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                    <label class="form-check-label" for="remember">
                                        {{ __('Remember Me') }}
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Login') }}
                                </button>

                                @if (Route::has('password.request'))
                                    <a class="btn btn-link" href="{{ route('password.request') }}">
                                        {{ __('Forgot Your Password?') }}
                                    </a>
                                @endif
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

user側のviewも同じようにパスを変更します。

おわりに

マルチ認証の実装方法の記事はたくさんあったのですが、6.x以降の記事は意外と少ないのですね…。 Qiitaなどを参考にさせていただきましたが、それでもnamespaceやパスを間違えたり何なりで失敗をしてなかなかうまくいかず苦しんだので、気をつけます…。 ひとまず今回ログイン画面をユーザ用と管理者用で分けるところまではできたので、登録画面やパスワードリセット機能等も作り込んでみたいです。