LoginSignup
2
8

More than 3 years have passed since last update.

【Laravel7でユーザー認証_10】マルチ認証機能で作成した管理者のパスワード変更・リセット機能を追加する

Last updated at Posted at 2020-07-06

はじめに

Laravelのユーザー認証機能に、管理者権限を追加(マルチ認証機能)した後、管理者のパスワードを変更・リセットする機能を追加する手順をまとめます。

環境

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

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

また、Laravelプロジェクトは以下の手順で作業を進めており、管理者はすでに作成されている状態です。

パスワード変更 実装手順

コントローラの作成

パスワード変更用のコントローラを作成します。
今回は、ユーザー用に作成した Auth/ChangePasswordControllerAdmin/Auth ディレクトリの中にコピーして利用します。

namespaceをAdmin用に書き換えます。
また、パスワード変更の処理はadminログインを必須としたいので、コンストラクトでチェックするように指定します。

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

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

  use App\Http\Controllers\Controller;
  use App\Http\Requests\ChangePasswordRequest;
  use Illuminate\Support\Facades\Auth;

  class ChangePasswordController extends Controller
  {
      public function __construct()
      {
-         $this->middleware('auth');
+         $this->middleware('auth:admin');
          $this->middleware('verified');
      }

      public function showChangePasswordForm()
      {
          return view('auth\passwords\change');
      }

      public function changePassword(ChangePasswordRequest $request)
      {
          //ValidationはChangePasswordRequestで処理
          //パスワード変更処理
          $user = Auth::user();
          $user->password = bcrypt($request->get('password'));
          $user->save();

          //homeにリダイレクト
          return redirect()->route('home')->with('status', __('Your password has been changed.'));
    }
  }

ルーティングの設定

以下の仕様で、ルーティングを設定します。

通常のアクセス(GET)の場合は、「Admin/Auth/ChangePasswordController」コントローラの「showChangePasswordForm」メソッドを実行。
パスワードを変更の処理(POST)の場合は、「Admin/Auth/ChangePasswordController」コントローラの「changePassword」メソッドを実行。
それぞれのルーティングには、「admin.password.form」、「admin.password.change」という名前を付けました。

/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');
+
+     Route::get('/setting/password', 'Auth\ChangePasswordController@showChangePasswordForm')->name('password.form');
+     Route::post('/setting/password', 'Auth\ChangePasswordController@changePassword')->name('password.change');
  });

viewの作成

viewは、auth/passwords/change.blade.php をコピーして利用します。
formのアクション先は、ルーティングで付けた「admin.password.change」という名前を使います。

resources/views/admin/auth/passwords/change.blade.php
@extends('layouts.app')

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

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

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

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

                                @error('current_password')
                                    <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">{{ __('New 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 New 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">
                                    {{ __('Change Password') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

viewを指定

コントローラの showChangePasswordForm メソッドで、作成したviewを指定します。

app/Http/Controllers/Admin/Auth/ChangePasswordController.php
      public function showChangePasswordForm()
      {
-         return view('auth\passwords\change');
+         return view('admin\auth\passwords\change');
      }

パスワード変更の処理

バリデーションチェック

FormRequest の設定

ユーザーのFormRequest app/Http/Requests/ChangePasswordRequest.phpapp/Http/Requests/Admin/ChangePasswordRequest.php に複製します。

app/Http/Request/Admin/ChangePasswordRequest.php
  <?php

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

  use Illuminate\Foundation\Http\FormRequest;
  use Illuminate\Contracts\Validation\Validator;
  use Illuminate\Http\Exceptions\HttpResponseException;
  use Illuminate\Support\Facades\Auth;
  use Illuminate\Support\Facades\Hash;

  class ChangePasswordRequest extends FormRequest
  {
      /**
       * Determine if the user is authorized to make this request.
       *
       * @return bool
       */
      public function authorize()
      {
          return true;
      }

      /**
       * Get the validation rules that apply to the request.
       *
       * @return array
       */
      public function rules()
      {
          return [
              'current_password' => ['required', 'string', 'min:8'],
              'password' => ['required', 'string', 'min:8', 'confirmed']
          ];
      }

      public function withValidator(Validator $validator) {
           $validator->after(function ($validator) {
               $auth = Auth::user();

               //現在のパスワードと新しいパスワードが合わなければエラー
               if (!(Hash::check($this->input('current_password'), $auth->password))) {
                   $validator->errors()->add('current_password', __('The current password is incorrect.'));
               }
           });
       }
  }

コントローラに読み込み

FormRequest をコントローラから読み込むように修正します。
また、パスワード変更時のリダイレクト先が管理者のhomeになるよう変更します。

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

  namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
- use App\Http\Requests\ChangePasswordRequest;
+ use App\Http\Requests\Admin\ChangePasswordRequest;
  use Illuminate\Support\Facades\Auth;

===(中略)===

      public function changePassword(ChangePasswordRequest $request)
      {
          //ValidationはChangePasswordRequestで処理
          //パスワード変更処理
          $user = Auth::user();
          $user->password = bcrypt($request->get('password'));
          $user->save();

          //homeにリダイレクト
-         return redirect()->route('home')->with('status', __('Your password has been changed.'));
+         return redirect()->route('admin.home')->with('status', __('Your password has been changed.'));
    }
  }

ヘッダからリンクする

ログインした後、右上に表示されるメニューの中に、パスワード変更のリンクを張っておきます。

resource/views/layouts/app.blade.php
                                  <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>
+
+                                     <a class="dropdown-item" href="{{ route('admin.password.form') }}">
+                                         {{ __('Change Password') }}
+                                     </a>
                                  </div>

動作確認

管理者のパスワード変更画面にアクセスして、希望の動作になるか確認します。
前回、secretという8桁以下のパスワードで設定したため、バリデーションがとおらなくなってしまいました。
tinkerを使ってDBを操作して、まずは直接パスワードを変更することにします。

$ php artisan tinker
$ \DB::table('admins')->where('id', 1)->update(['password' => \Hash::make('secretpassword')]);

これでパスワードが「secretpassword」に変更されました。
現在のパスワード欄に「secretpassword」、新しいパスワード欄に任意の文字列を入力して動作を確認します。


パスワードリセット 実装手順

ルーティングの定義

管理者のパスワードリセットについて、Auth::routes() を使うと、認証系に必要な定義を自動で追加してくれます。
管理者用は登録部分は利用しないことにするので、オプションで以下のように指定します。

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');

      Route::get('/setting/password', 'Auth\ChangePasswordController@showChangePasswordForm')->name('password.form');
      Route::post('/setting/password', 'Auth\ChangePasswordController@changePassword')->name('password.change');
+
+     Auth::routes([
+         'register' => false,
+         'reset'    => true,
+         'verify'   => false
+     ]);
  });

php artisan route:list をしてみると、admin.password.email や admin.password.update といったパスワードに関するルーティングが追加されているのが分かります。
Laravel-admin-resetpassword-01.fw.png

このルーティングに沿って、コントローラを作成していきます。

コントローラの作成

ユーザー用のパスワードリセット用のコントローラ app/Http/Controllers/Auth/ForgotPasswordController.phpapp/Http/Controllers/Auth/ResetPasswordController.php を、管理者用として app/Http/Controllers/Admin/Auth/ 以下にコピーします。

コピー後、namespaceやリダイレクト先を管理者向けに変更します。

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

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

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

  class ForgotPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset emails and
      | includes a trait which assists in sending these notifications from
      | your application to your users. Feel free to explore this trait.
      |
      */

      use SendsPasswordResetEmails;
  }
app/Http/Controllers/Admin/Auth/ResetPasswordController.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\ResetsPasswords;

  class ResetPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset requests
      | and uses a simple trait to include this behavior. You're free to
      | explore this trait and override any methods you wish to tweak.
      |
      */

      use ResetsPasswords;

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

viewの作成

ユーザー用のパスワードリセット用のview resources/views/auth/passwords/reset.blade.phpresources/views/auth/passwords/email.blade.php を、管理者用として app/Http/Controllers/Admin/Auth/ 以下にコピーします。
コピー後、レイアウトやformの送信先を変更します。

resources/views/auth/passwords/reset.blade.php
- @extends('layouts.app')
+ @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">{{ __('Reset Password') }}</div>

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

                          <input type="hidden" name="token" value="{{ $token }}">

                          <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="{{ $email ?? 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="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">
                                      {{ __('Reset Password') }}
                                  </button>
                              </div>
                          </div>
                      </form>
                  </div>
              </div>
          </div>
      </div>
  </div>
  @endsection
resources/views/auth/passwords/email.blade.php
- @extends('layouts.app')
+ @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">{{ __('Reset Password') }}</div>

                  <div class="card-body">
                      @if (session('status'))
                          <div class="alert alert-success" role="alert">
                              {{ session('status') }}
                          </div>
                      @endif
+
-                     <form method="POST" action="{{ route('password.email') }}">
+                     <form method="POST" action="{{ route('admin.password.email') }}">
                          @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 mb-0">
                              <div class="col-md-6 offset-md-4">
                                  <button type="submit" class="btn btn-primary">
                                      {{ __('Send Password Reset Link') }}
                                  </button>
                              </div>
                          </div>
                      </form>
                  </div>
              </div>
          </div>
      </div>
  </div>
  @endsection

管理者用パスワードリセットトークン保存テーブル作成

ユーザー用のパスワードリセットトークン保存テーブル password_resets と、管理者用のものを分けるため、admin_password_resets というテーブルを新たに作成します。

php artisan make:migration create_admin_password_resets_table 

日付_create_admin_password_resets_table.php の中身は、 日付_create_password_resets_table.php を参考にします。

database/migrations/日付_create_admin_password_resets_table.php
<?php

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

class CreateAdminPasswordResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admin_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

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

パスワードブローカーの設定

管理者がパスワードリセット時に admin_password_resets テーブルを使用するため、config/auth.php で、管理者用のパスワードブローカーを設定します。

config/auth.php
      'passwords' => [
          'users' => [
              'provider' => 'users',
              'table' => 'password_resets',
              'expire' => 60,
              'throttle' => 60,
        'admins' => [
            'provider' => 'admins',
            'table' => 'admin_password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
          ],
      ],

configファイルを変更したので、キャッシュをクリアします。

php artisan config:cache

コントローラの修正

パスワードリセットのボタンを押すと、ForgotPasswordControllershowLinkRequestForm() が実行されます。現状だと、SendsPasswordResetEmailsvendor/laravel/ui/auth-backend/SendsPasswordResetEmails.php) に処理がまとめられているため、このままだと管理者もユーザーと同じ挙動になってしまいます。
管理者は管理者向けの処理になるよう、ForgotPasswordController 内で各メソッドをオーバーライドします。

app\Http\Controllers\Admin\Auth\ForgotPasswordController.php
  <?php

  namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
+ use Illuminate\Support\Facades\Password;

  class ForgotPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset emails and
      | includes a trait which assists in sending these notifications from
      | your application to your users. Feel free to explore this trait.
      |
      */

      use SendsPasswordResetEmails;
+
+     public function showLinkRequestForm()
+     {
+         return view('admin.auth.passwords.email');
+     }
+
+     public function broker()
+     {
+         return Password::broker('admins');
+     }
}
app\Http\Controllers\Admin\Auth\ResetPasswordController.php
  <?php

  namespace App\Http\Controllers\Admin\Auth;

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

  class ResetPasswordController extends Controller
  {

  ==(中略)==

      protected $redirectTo = RouteServiceProvider::ADMIN_HOME;
+
+     public function showResetForm(Request $request, $token = null)
+     {
+         return view('admin.auth.passwords.reset')->with(
+             ['token' => $token, 'email' => $request->email]
+         );
+     }
+
+     public function broker()
+     {
+         return Password::broker('admins');
+     }
}

リセットメールの変更

ここまでで一旦動作確認してみると、パスワードリセットメールは届きますが、本文中のURLがユーザー用のURLのままです。
管理者用の通知メールを作成し、そちらを利用するようにします。

Notificationの作成

php artisan make:notification コマンドを実行します。

php artisan make:notification AdminResetPasswordNotification

app/Notifications/AdminResetPasswordNotification.php が作成されるので、内容を修正します。
メールの送信は、 vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php の中の toMail() を使っているので、このメソッドをコピーしてURL部分だけ管理者用に変更しました。

app/Notifications/AdminResetPasswordNotification.php
  <?php

  namespace App\Notifications;

  use Illuminate\Bus\Queueable;
  use Illuminate\Contracts\Queue\ShouldQueue;
  use Illuminate\Notifications\Messages\MailMessage;
  use Illuminate\Notifications\Notification;
+ use Illuminate\Support\Facades\Lang;

  class AdminResetPasswordNotification extends Notification
  {
      use Queueable;
+     public static $toMailCallback;
+     public static $createUrlCallback;

      /**
       * Create a new notification instance.
       *
       * @return void
       */
-     public function __construct()
+     public function __construct($token)
      {
+         $this->token = $token;
      }

  ===(中略)===

      public function toMail($notifiable)
      {
          return (new MailMessage)
-                     ->line('The introduction to the notification.')
-                     ->action('Notification Action', url('/'))
-                     ->line('Thank you for using our application!');
+         if (static::$toMailCallback) {
+             return call_user_func(static::$toMailCallback, $notifiable, $this->token);
+         }
+ 
+         if (static::$createUrlCallback) {
+             $url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
+         } else {
+             $url = url(route('password.reset', [
+                 'token' => $this->token,
+                 'email' => $notifiable->getEmailForPasswordReset(),
+             ], false));
+         }
+ 
+         return (new MailMessage)
+             ->subject(Lang::get('Reset Password Notification'))
+             ->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
+             ->action(Lang::get('Reset Password'), $url)
+             ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
+             ->line(Lang::get('If you did not request a password reset, no further action is required.'));
        }

Adminモデルの修正

管理者は AdminResetPasswordNotification を使うように、Adminモデルを修正します。

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;
+ use App\Notifications\AdminResetPasswordNotification;

  class Admin extends Authenticatable implements MustVerifyEmail
  {

  ===(中略)===
+
+     public function AdminResetPasswordNotification($token)
+     {
+             $this->notify(new AdminResetPasswordNotification($token));
+     }
  }

おわりに

ここまでで、管理者のログイン・パスワード変更・パスワードリセットができるようになりました。
今回、管理者の登録・削除は作成しませんでしたが、同じようにして作成できそうです。
ユーザー認証についてのまとめは、ここまでで一旦完了としたいと思います。

参考サイト

2
8
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
2
8