Laravel 7.xでマルチ認証機能を作成してみた 2 〜ユーザ登録・パスワード再設定〜

tech.arms-soft.co.jp

加藤です。前回Laravel 7.xでマルチ認証機能を作成し、一般ユーザ用と管理者用とでログイン画面を分けることに成功しました。 今回は登録とパスワードリセット機能をそれぞれに追加していこうと思います。

環境

  • PHP 7.3
  • Laravel 7.0

目標・やりたいこと

  1. 一般ユーザのユーザ登録とパスワードリセット機能の実装
  2. 管理者ユーザのユーザ登録とパスワードリセット機能の実装

なお、通常は外部から管理者ユーザを登録することは無いと思いますが、ここではマルチ認証機能の練習のためにそうしています。(adminではない何か別の名前にすればよかったな…と後悔しました…。)

ユーザ登録

(1)CotrollerとViewの準備

前回実行した以下のコマンド

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

で、すでにApp\Http\Controllers\Auth配下には一般ユーザ用のControllerとViewができています。

作られているRegisterControllerとregister.blade.php のファイルをコピーして、Admin用のControllerとViewをそれぞれ用意します。 階層構造は以下の通りです。

Controller

app
|
|- Http
   |- Controllers
       |- Admin                                 // 管理者ユーザ用のController
           |- Auth
              |- LoginController.php
              |- RegisterController.php        // 登録(コピーして作成)
           |- HomeController.php
       |
       |- Auth                                  // 一般ユーザ用のController
           |- LoginController.php
          |- RegisterController.php            // 登録
       |- HomeController.php             

View

resource
   |- views
      |
      |- admin                                   // 管理者ユーザ用のview
         |- auth
             |- login.blade.php
             |- register.blade.php             // 登録(コピーして作成)
         |- home.blade.php
      |
      |- layouts
         |- admin
             |- app.blade.php
         |- user
             |- app.blade.php
      |- user                                   // 一般ユーザ用のview
         |-auth
             |- login.blade.php
             |- register.blade.php             // 登録
         |- home.blade.php

(2)RegisterControllerの準備

RegisterControllerはユーザ登録の標準クラスであるRegistersUsersを継承していますが、一般ユーザと管理者用のユーザとでViewやModelを分けているため、そのままでは使えません。

showRegistrationForm()とcreate()をオーバーライドして、一般ユーザ用のControllerからは一般ユーザ用のViewとModelを、管理者ユーザ用のControllerからは管理者ユーザ用のViewとModelを見に行くようにそれぞれ変更します。

validator()もオーバーライドすることで独自の制限を設けることができます。

一般ユーザ用 app\Http\Controllers\Auth\RegisterController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;

class RegisterController extends Controller
{
    use RegistersUsers;

    // 一般ユーザ用のホーム画面へのリダイレクト
    protected $redirectTo = RouteServiceProvider::HOME;


    // 一般ユーザ用のguardを指定
    protected function guard()
    {
        return Auth::guard('user');
    }

    // 一般ユーザ登録用のviewを指定
    public function showRegistrationForm()
    {
        return view('user.auth.register');
    }

    // 登録時のバリデーション
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    // 登録
    protected function create(array $data)
    {
        // 一般ユーザ用のUser Modelを指定
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

管理者ユーザ用 app\Http\Controllers\Admin\Auth\RegisterController.php

<?php

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

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Admin;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;

class RegisterController extends Controller
{
    use RegistersUsers;

    // 管理者用のホーム画面へのリダイレクト
    protected $redirectTo = RouteServiceProvider::ADMIN_HOME;


    // 管理者用のguardを指定
    protected function guard()
    {
        return Auth::guard('admin');
    }

    // 管理者ユーザ登録用のviewを指定
    public function showRegistrationForm()
    {
        return view('admin.auth.register');
    }

    // 登録時のバリデーション
   protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    // 登録
    protected function create(array $data)
    {
       // 管理者ユーザ用のAdmin Modelを指定
        return Admin::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

(3)registerのルーティングを追加

ユーザ用、管理者用それぞれの登録画面へのルーティング設定をします。 前回falseにしていたregisterをtrueに変えます。

routes/web.php

<?php

use Illuminate\Support\Facades\Route;

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

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' => true,  //ここをtrueに
        'reset'    => false,
        'verify'   => false
    ]);
    Route::middleware('auth:admin')->group(function () {
        Route::get('home', 'HomeController@index')->name('home');
    });
});

(4)View内のパスを合わせる

コピーして作成したregister.blade.phpの layoutsのextendsやログインform内のパスを合うように変更します。

管理者用 resources/views/admin/auth/register.blade.php

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

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

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

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

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

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

                        <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">

                                @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="new-password">

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

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

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                        </div>

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

User側も同じ様にパスを変更します。

パスワードリセット

(1)CotrollerとViewの準備

パスワードリセットに必要なControllerとViewである

  • ForgotPasswordController.php
  • ResetPasswordController.php
  • email.blade.php
  • reset.blade.php

も一般ユーザ用のControllerとViewが用意されているので、Registerのときと同様にファイルをコピーして管理者ユーザ用のものを用意しておきます。

階層構造は以下の通りです。

Controller

app
|
|- Http
   |- Controllers
       |- Admin                                    // 管理者ユーザ用のController
           |- Auth
              |- LoginController.php
              |- RegisterController.php
              |- ForgotPasswordController.php     // パスワード再設定メール送信(コピーして作成)
              |- ResetPasswordController.php      // パスワード再設定(コピーして作成)
           |- HomeController.php
       |
       |- Auth                                    // 一般ユーザ用のController
           |- LoginController.php
          |- RegisterController.php
          |- ForgotPasswordController.php        // パスワード再設定メール送信
          |- ResetPasswordController.php          // パスワード再設定
       |- HomeController.php             

View

resource
   |- views
      |
      |- admin                           // 管理者ユーザ用のview
         |- auth
             |- login.blade.php
             |- register.blade.php
             |- passwords
                 |- email.blade.php    // パスワード再設定メール送信(コピーして作成)
                 |- reset.blade.php    // パスワード再設定(コピーして作成)
         |- home.blade.php
      |
      |- layouts
         |- admin
             |- app.blade.php
         |- user
             |- app.blade.php
      |- user                            // 一般ユーザ用のview
         |-auth
             |- login.blade.php
             |- register.blade.php
             |- passwords
                 |- email.blade.php    // パスワード再設定メール送信
                 |- reset.blade.php    // パスワード再設定
         |- home.blade.php

(2)メールの設定を定義する

ForgotPasswordControllerではユーザのメールアドレスに対してリセット用URLが記述されたパスワード再設定メールを送信するので、事前にメールサーバの設定が必要です。 config/mail.phpや.envファイルでメールサーバの情報を定義しておきます。

(3)管理者用のパスワードリセットトークン保存用テーブルの追加作成

発行されたパスワードリセットトークンを一般ユーザ用と管理者用と別々に保存しておくため、admin_password_resetsテーブルを作成しておきます。 password_resetsテーブルのcreateのmigrationファイルをコピーして、artisan migrate コマンド実行で作成できます。

<?php
class CreateAdminPasswordResetsTable extends Migration
{
    public function up()
    {
        Schema::create('admin_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    public function down()
    {
        Schema::dropIfExists('admin_password_resets');
    }
}
php artisan migrate 

(4)管理者用のパスワードブローカーの定義

前回も触れたauth.phpです。 一般ユーザと管理者ユーザそれぞれに対してパスワードリセット機能を提供するため、passwordsにadmin用のパスワードブローカを定義します。 config/auth.php

<?php 

'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'admins' => [                            // 追加
            'provider' => 'admins',
            'table' => 'admin_password_resets',    // さきほど追加作成したテーブルを指定
            'expire' => 60,
            'throttle' => 60,
        ]
]

(5)ForgotPasswordControllerの準備

パスワード再設定メール送信用のControllerです。 ここでも標準クラスであるSendsPasswordResetEmailsを継承していますが、一般ユーザと管理者ユーザとでViewとパスワードブローカーを分けたいため、そのままでは使えません。

showLinkRequestForm()をオーバーライドして、一般ユーザ用のControllerからは一般ユーザ用のViewを、管理者ユーザ用のControllerからは管理者ユーザ用のViewを返却するようにそれぞれ設定します。

また、broker()もオーバーライドして、パスワードブローカーも分ける様にします。

一般ユーザ用 app\Http\Controllers\Auth\ForgotPasswordController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

class ForgotPasswordController extends Controller
{
    
    use SendsPasswordResetEmails;

    // 一般ユーザ用のviewを指定
    public function showLinkRequestForm()
    {
        return view('user.auth.passwords.email');
    }

    
    public function broker()
    {
        // 一般ユーザ用のパスワードブローカーを指定
        return Password::broker('users');
    }
}

管理者ユーザ用 app\Http\Controllers\Admin\Auth\ ForgotPasswordController.php

<?php

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

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

class ForgotPasswordController extends Controller
{

    use SendsPasswordResetEmails;

    // 管理者ユーザ用のviewを指定
    public function showLinkRequestForm()
    {
        return view('admin.auth.passwords.email');
    }

    public function broker()
    {
        // 管理者ユーザ用のパスワードブローカーを指定
        return Password::broker('admins');
    }
}

(6)ResetPasswordControllerの準備

パスワード再設定用のControllerです。 ResetPasswordControllerでも標準クラスであるResetsPasswordsを継承していますが、 ここでもViewとパスワードブローカーを一般用と管理者用とで分けて指定する必要があります。

ForgotPasswordControllerの時と同様に、showResetForm()とbroker()をオーバーライドします。

一般ユーザ用 app\Http\Controllers\Auth\ResetPasswordController.php

<?php

namespace App\Http\Controllers\Auth;

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

class ResetPasswordController extends Controller
{
    use ResetsPasswords;

    protected $redirectTo = RouteServiceProvider::HOME;

    public function showResetForm(Request $request, $token = null)
    {
        // 一般ユーザ用のviewを指定
        return view('user.auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }

    public function broker()
    {
        // 一般ユーザ用のパスワードブローカーを指定
        return Password::broker('users');
    }
}

管理者ユーザ用 app\Http\Controllers\Admin\Auth\ ForgotPasswordController.php

<?php

namespace App\Http\Controllers\Admin\Auth;

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

class ResetPasswordController extends Controller
{
    use ResetsPasswords;

    protected $redirectTo = RouteServiceProvider::ADMIN_HOME;

    public function showResetForm(Request $request, $token = null)
    {
        // 管理者ユーザ用のviewを指定
        return view('admin.auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }

    public function broker()
    {
        // 管理者ユーザ用のパスワードブローカーを指定
        return Password::broker('admins');
    }
}

おわりに

勉強のために、なるべくlaravelインストール時のデフォルトから手を入れないようにして、マルチ認証環境でのユーザ登録・パスワードリセットの基本的な実装方法のみに絞ってまとめました。 標準クラスからメソッドをオーバーライドすることによってView・Model振り分けやバリデーションなどの独自処理を追加してカスタマイズができることがわかりました。 一回のコマンド実行で構築できるlaravelフレームワークのありがたみを感じました…。

今回は以上です。