#はじめに
Laravelのユーザー認証機能に、管理者権限を追加(マルチ認証機能)した後、管理者のパスワードを変更・リセットする機能を追加する手順をまとめます。
環境
XAMPP環境でLaravelが使えるように設定してあります。
- Windows10 Pro 64bit
- PHP 7.3.18
- Laravel 7.12.0
- MariaDB 10.1.32
また、Laravelプロジェクトは以下の手順で作業を進めており、管理者はすでに作成されている状態です。
- 【Laravel7でユーザー認証_1】基本のき
- 【Laravel7でユーザー認証_2】ユーザー認証を日本語化
- 【Laravel7でユーザー認証_3】ユーザー認証をメールアドレスからユーザー名に変更する
- 【Laravel7でユーザー認証_4】パスワード変更フォームを作成する
- 【Laravel7でユーザー認証_5】ユーザーを倫理削除(SoftDelete)する
- 【Laravel7でユーザー認証_6】ユーザーの情報を表示・変更する設定画面を作成する
- 【Laravel7でユーザー認証_7】会員登録時にメール認証を行う
- 【Laravel7でユーザー認証_8】メールアドレス変更時にメール認証を行う
- 【Laravel7でユーザー認証_9】マルチ認証機能を使って管理者を作成する
#パスワード変更 実装手順
##コントローラの作成
パスワード変更用のコントローラを作成します。
今回は、ユーザー用に作成した Auth/ChangePasswordController
を Admin/Auth
ディレクトリの中にコピーして利用します。
namespaceをAdmin用に書き換えます。
また、パスワード変更の処理はadminログインを必須としたいので、コンストラクトでチェックするように指定します。
<?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」という名前を付けました。
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」という名前を使います。
@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を指定します。
public function showChangePasswordForm()
{
- return view('auth\passwords\change');
+ return view('admin\auth\passwords\change');
}
##パスワード変更の処理
###バリデーションチェック
####FormRequest の設定
ユーザーのFormRequest app/Http/Requests/ChangePasswordRequest.php
をapp/Http/Requests/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になるよう変更します。
<?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.'));
}
}
##ヘッダからリンクする
ログインした後、右上に表示されるメニューの中に、パスワード変更のリンクを張っておきます。
<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()
を使うと、認証系に必要な定義を自動で追加してくれます。
管理者用は登録部分は利用しないことにするので、オプションで以下のように指定します。
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 といったパスワードに関するルーティングが追加されているのが分かります。
このルーティングに沿って、コントローラを作成していきます。
##コントローラの作成
ユーザー用のパスワードリセット用のコントローラ app/Http/Controllers/Auth/ForgotPasswordController.php
と app/Http/Controllers/Auth/ResetPasswordController.php
を、管理者用として app/Http/Controllers/Admin/Auth/
以下にコピーします。
コピー後、namespaceやリダイレクト先を管理者向けに変更します。
<?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;
}
<?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.php
と resources/views/auth/passwords/email.blade.php
を、管理者用として app/Http/Controllers/Admin/Auth/
以下にコピーします。
コピー後、レイアウトやformの送信先を変更します。
- @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
- @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
を参考にします。
<?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
で、管理者用のパスワードブローカーを設定します。
'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
##コントローラの修正
パスワードリセットのボタンを押すと、ForgotPasswordController
の showLinkRequestForm()
が実行されます。現状だと、SendsPasswordResetEmails
(vendor/laravel/ui/auth-backend/SendsPasswordResetEmails.php
) に処理がまとめられているため、このままだと管理者もユーザーと同じ挙動になってしまいます。
管理者は管理者向けの処理になるよう、ForgotPasswordController
内で各メソッドをオーバーライドします。
<?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');
+ }
}
<?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部分だけ管理者用に変更しました。
<?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モデルを修正します。
<?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));
+ }
}
#おわりに
ここまでで、管理者のログイン・パスワード変更・パスワードリセットができるようになりました。
今回、管理者の登録・削除は作成しませんでしたが、同じようにして作成できそうです。
ユーザー認証についてのまとめは、ここまでで一旦完了としたいと思います。
#参考サイト