LoginSignup
3
5

More than 3 years have passed since last update.

【Laravel7でユーザー認証_9】マルチ認証機能を使って管理者を作成する

Last updated at Posted at 2020-06-29

はじめに

Laravelのユーザー認証機能に、管理者権限を追加する(マルチ認証機能を使う)手順をまとめます。

環境

XAMPP環境でLaravelが使えるように設定してあります。

  • Windows10 Pro 64bit
  • PHP 7.3.18
  • Laravel 7.12.0
  • MariaDB 10.1.32

また、Laravelプロジェクトは以下の手順で作業を進めており、ユーザーは自身で情報を登録・変更・削除ができるようになっています。

実装手順

Adminモデルとadminsテーブルの作成

以下のコマンドで、Adminモデルとマイグレーションファイルを作成します。

php artisan make:model Admin -m

Adminモデルは、Userモデルの内容をコピーして使います。

app/Admin.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Admin extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
            'name', 'email', 'username', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
            'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
            'email_verified_at' => 'datetime',
    ];
}

マイグレーションファイルも、現在のusersテーブルの仕様に合わせます。

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
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('username', 32);
            $table->string('name');
            $table->string('email');
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
            $table->softDeletes();
            $table->unique(['email', 'deleted_at'], 'admins_email_deleted_at_unique');
            $table->unique(['username', 'deleted_at'], 'admins_username_deleted_at_unique');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

マイグレーションを実行すると、usersと同じ構造のadminsテーブルが出来上がります。

php artisan migrate

管理者用のガードとプロバイダを追加

認証の設定ファイル config/auth.php に、adminが使うガードとプロバイダを設定します。

config/auth.php
      'guards' => [
          'web' => [
              'driver' => 'session',
              'provider' => 'users',
          ],
+ 
+         'admin' => [
+             'driver' => 'session',
+             'provider' => 'admins',
+         ],

          'api' => [
              'driver' => 'token',
              'provider' => 'users',
              'hash' => false,
          ],
      ],

      /*
      |--------------------------------------------------------------------------
      | User Providers
      |--------------------------------------------------------------------------
      |
      | All authentication drivers have a user provider. This defines how the
      | users are actually retrieved out of your database or other storage
      | mechanisms used by this application to persist your user's data.
      |
      | If you have multiple user tables or models you may configure multiple
      | sources which represent each model / table. These sources may then
      | be assigned to any extra authentication guards you have defined.
      |
      | Supported: "database", "eloquent"
      |
      */

      'providers' => [
          'users' => [
              'driver' => 'eloquent',
              'model' => App\User::class,
          ],
+ 
+         'admins' => [
+             'driver' => 'eloquent',
+             'model' => App\Admin::class,
+         ],

          // 'users' => [
          //     'driver' => 'database',
          //     'table' => 'users',
          // ],
      ],

configファイル変更後は、キャッシュをクリアしておきます。

php artisan config:cache

ADMIN_HOME の定義

管理者用のhomeとして、ADMIN_HOMEを定義します。

app/Providers/RouteServiceProvider.php
      /**
       * The path to the "home" route for your application.
       *
       * @var string
       */
      public const HOME = '/home';
+     public const ADMIN_HOME = '/admin/home';

リダイレクト処理

未ログイン時のリダイレクト

管理者としてログインしていない閲覧者が管理者権限が必要なページにアクセスした場合、ログイン画面にリダイレクトさせる設定をします。

app/Http/Middleware/Authenticate.php
+ use Illuminate\Support\Facades\Route;

  class Authenticate extends Middleware
  {
      /**
       * Get the path the user should be redirected to when they are not authenticated.
       *
       * @param  \Illuminate\Http\Request  $request
       * @return string|null
       */
      protected function redirectTo($request)
      {
          if (! $request->expectsJson()) {
-             return route('login');
+             if (Route::is('admin.*')) {
+                 return route('admin.login');
+             } else {
+                 return route('login');
+             }
          }
      }

管理者ログイン直後のリダイレクト

管理者としてログインした際、ADMIN_HOMEにリダイレクトさせるよう設定します。

app/Http/Middleware/RedirectIfAuthenticated.php
      /**
       * Handle an incoming request.
       *
       * @param  \Illuminate\Http\Request  $request
       * @param  \Closure  $next
       * @param  string|null  $guard
       * @return mixed
       */
      public function handle($request, Closure $next, $guard = null)
      {
          if (Auth::guard($guard)->check()) {
-             return redirect(RouteServiceProvider::HOME);
+             if ($guard === 'user') {
+                 return redirect(RouteServiceProvider::HOME);
+             } elseif ( $guard === 'admin') {
+                 return redirect(RouteServiceProvider::ADMIN_HOME);
+             }
          }

          return $next($request);
      }

コントローラ作成

ユーザー用のコントローラをコピーして、管理者用の HomeControllerLoginController を作成します。

コピー元:app/Http/Controllers/HomeController.php
コピー先:app/Http/Controllers/Admin/HomeController.php

app/Http/Controllers/Admin/HomeController.php
  <?php

- namespace App\Http\Controllers;
+ namespace App\Http\Controllers\Admin;

  use Illuminate\Http\Request;

  class HomeController.php extends Controller
  {
      /**
       * Create a new controller instance.
       *
       * @return void
       */
      public function __construct()
      {
-         $this->middleware('auth');
+         $this->middleware('auth:admin');
      }

      /**
       * Show the application dashboard.
       *
       * @return \Illuminate\Contracts\Support\Renderable
       */
      public function index()
      {
-         return view('home');
+         return view('admin.home');
      }
  }

コピー元:app/Http/Controllers/Auth/LoginController.php
コピー先:app/Http/Controllers/Admin/Auth/LoginController.php

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

- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

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

  class LoginController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Login Controller
      |--------------------------------------------------------------------------
      |
      | This controller handles authenticating users for the application and
      | redirecting them to your home screen. The controller uses a trait
      | to conveniently provide its functionality to your applications.
      |
      */

      use AuthenticatesUsers;

      /**
       * Where to redirect users after login.
       *
       * @var string
       */
-     protected $redirectTo = RouteServiceProvider::HOME;
+     protected $redirectTo = RouteServiceProvider::ADMIN_HOME;

      /**
       * Create a new controller instance.
       *
       * @return void
       */
      public function __construct()
      {
-         $this->middleware('guest')->except('logout');
+         $this->middleware('guest:admin')->except('logout');
      }
+
+     protected function guard()
+     {
+         return Auth::guard('admin');
+     }
+
+     //ログインフォームのview指定
+     public function showLoginForm()
+     {
+         return view('admin.auth.login');
+     }
+
+     //ログアウトの処理
+     public function logout(Request $request)
+     {
+         Auth::guard('admin')->logout();
+         return $this->loggedOut($request);
+     }
+
+     //ログアウト時のリダイレクト先
+     public function loggedOut(Request $request)
+     {
+         return redirect(route('admin.login'));
+     }

      public function username()
      {
          return 'username';
      }
  }

ルーティングの設定

  • App\Http\Controllers\Admin 名前空間下のコントローラを読み込ませたいので、namespace メソッドで Admin を指定
  • 管理者用のURIは、admin/ でまとめたいので、prefix メソッドで admin を指定
  • ルート名もまとめてadmin.というプリフィックスをつけたいので、nameメソッドで admin. を指定
routes/web.php
+ Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function() {
+     Route::get('home', 'HomeController@index')->name('home');
+     Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
+     Route::post('login', 'Auth\LoginController@login');
+     Route::post('logout', 'Auth\LoginController@logout')->name('logout');
+ }

管理者用のviewを作成

管理者用とユーザー用の画面がわかりやすいように、管理者はダークモードにしたいと思います。
viewの構成は、以下のようにしました。


views/
  ├ admin/
  │  ├ auth/
  │  │  └ login.blade.php  #resources/views/auth/login.blade.phpをコピー
  │  └ home.blade.php  #resources/views/home.blade.phpをコピー
  │
  └ layouts/
     └ admin/
       └ app.blade.php  #resources/views/layouts/app.blade.phpをコピー

@guest で判定していた箇所は、 @if (Auth::guard('admin')->check()) で、管理者かどうかのチェックをするように変更しました。

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-dark bg-dark 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 -->
                        @if (Auth::guard('admin')->check())
                            <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>
                                    {{ Auth::user()->name }} <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;">
                                        @csrf
                                    </form>

                                </div>
                            </li>
                        @else
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
                            </li>
                        @endif
                    </ul>
                </div>
            </div>
        </nav>

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

▼ヘッダ部分について、作成した layout/admin/app.blade.php を読み込むようにします。

resources/views/admin/home.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">Dashboard</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    You are logged in!
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

▼ヘッダ部分について、作成した layout/admin/app.blade.php を読み込むようにして、formのaction先もadminのloginとなるようにルートを指定します。

resources/views/admin/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">{{ __('Admin') }}{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.login') }}">
                        @csrf

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

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

                                @error('username')
                                    <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">
                                    {{ __('Admin') }}{{ __('Login') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

翻訳ファイルに追記

view内で新たにAdminという翻訳文字列を使ったので、翻訳ファイルに追加します。

resources/lang/ja.json
+     "Admin": "管理者"
  }

管理者情報の登録

http://127.0.0.1:8000/admin/login にアクセスすると、管理者ログインフォームが表示されるまではできあがりました。
ですが、管理者の登録画面を作っていないので、このままだとログインも登録もできない状態です。
まずはログインができるかどうかを試したいので、データベースに管理者のデータを直接入れ込んでしまって確認します。

SQLで直接データを入れてしまってもいいのですが、せっかくなのでLaravelのシーダー機能を使ってデータを入れてみます。

シーダーファイルの作成

AdminsTableSeeder というシーダーファイルを作成します。

php artisan make:seeder AdminsTableSeeder

adminsテーブルに入っていてほしいデータを指定します。

項目 カラム名
氏名 name 管理者
ユーザー名 username admin
メールアドレス email admin@example.com
パスワード password secret
database/seeds/AdminsTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class AdminsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
      DB::table('admins')->delete();

      DB::table('admins')->insert([
             [
                  'name'      => '管理者',
                  'username'  => 'admin',
                  'email'     => 'admin@example.com',
                  'email_verified_at' => date('Y-m-d H:i:s'),

                  'password' => Hash::make('secret'),

                  'created_at' => date('Y-m-d H:i:s'),
                  'updated_at' => date('Y-m-d H:i:s') 
               ]
          ]);
    }
}
database/seeds/DatabaseSeeder.php
      public function run()
      {
-         //
+         $this->call(AdminsTableSeeder::class);
      }

シーダーを実行して、データが入ったかどうか確認します。

php artisan db:seed

動作確認

設定した情報でログインができるかどうか確認します。

▼私はこんなエラーと戦いました。

フォーム表示→ Auth guard [admin] is not defined.
config/app.php を変更した後、php artisan config:cache をしていませんでした。

ログイン→ Class 'App\Http\Controllers\Admin\Auth\Auth' not found

app/Http/Controllers/Admin/Auth/LoginController.php に、use Illuminate\Support\Facades\Auth; が抜けていました。

ログイン→ Class 'App\Http\Controllers\Admin\Controller' not found

app/Http/Controllers/Admin/HomeController.php に、use App\Http\Controllers\Controller; が抜けていました。

未ログイン状態で /admin/home にアクセス→ Class 'App\Http\Middleware\Route' not found

app/Http/Controllers/Middleware/Authenticate.phpuse Illuminate\Support\Facades\Route; が抜けていました。
リダイレクト分岐判定のRouteの前にバックスラッシュを置いてグローバルクラスにすれば( if (\Route::is('admin.*')) { )useを使わなくてもよいようです。

おわりに

管理者のログイン、ホームの表示ができるようになりました。
機能としてはまだ不十分なので、次回は、管理者のパスワード変更やリセットができるように修正したいと思います。

参考サイト

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5