こんにちは。Laravelエンジニアのタクマです。
今回は、Laravelのパスワードリセット機能の実装方法を解説します。
・パスワードリセット機能の実装方法がわからない
・有効期限つきメールの作成方法がわからない
こんなお悩みを解決できる記事となっております。
実装するパスワードリセット機能は以下のような流れのものになります。
- パスワードリセットを希望するユーザーにパスワードリセットメールを送信
- パスワードリセットメールに添付する署名付きURLには有効期限を設定
- 有効期限内にパスワードリセットメールに添付されている署名付きURLにアクセスをし、パスワードを更新する
上記の流れを実装しながら詳しく解説していきます。
前提条件
- passwordカラムを含むusersテーブルは作成されているものとする
- userのログイン機能は実装できているものとする
- バージョン:Laravel8.0
それではさっそく解説していきます。
usert_tokensテーブルの作成
まず、user_tokens
テーブルを作成します。
php artisan make:migration create_user_tokens_table
生成されたマイグレーションファイルを下記のように編集します。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class CreateUserTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_tokens', function (Blueprint $table) {
$table->id()->comment('ID');
$table->foreignId('user_id')->constrained()->comment('ユーザーのID');
$table->string('token')->unique()->comment('トークン');
$table->dateTime('expire_at')->nullable()->comment('トークンの有効期限');
$table->timestamps();
});
DB::statement("ALTER TABLE user_tokens COMMENT 'ユーザートークン'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_tokens');
}
}
user_id
には外部キー制約を設定しています。
外部キー制約について詳しく知りたい方は、僕が以前書いたこちらの記事をご覧ください。

トークンは重複しないよう、ユニーク制約を設定しています。
DB::statement("ALTER TABLE user_tokens COMMENT 'ユーザートークン'");
ではテーブルにコメントを付けています。
参考:[Laravel] MySQL のテーブルにコメントを付けるには ?
マイグレーションファイルを編集したらマイグレートし、user_tokens
テーブルを作成します。
php artisan migrate
これで、データベースにuser_tokens
テーブルが作成できました。
UserTokenモデルの作成
UserToken
モデルを作成します。
php artisan make:model UserToken
生成されたUserToken.php
を下記のように編集します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserToken extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_id',
'token',
'expire_at',
];
}
ルーティングの設定
ルーティングを設定します。
web.php
に下記を追加してください。
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PasswordController;
// パスワードリセット関連
Route::prefix('password_reset')->name('password_reset.')->group(function () {
Route::prefix('email')->name('email.')->group(function () {
// パスワードリセットメール送信フォームページ
Route::get('/', [PasswordController::class, 'emailFormResetPassword'])->name('form');
// メール送信処理
Route::post('/', [PasswordController::class, 'sendEmailResetPassword'])->name('send');
// メール送信完了ページ
Route::get('/send_complete', [PasswordController::class, 'sendComplete'])->name('send_complete');
});
// パスワード再設定ページ
Route::get('/edit', [PasswordController::class, 'edit'])->name('edit');
// パスワード更新処理
Route::post('/update', [PasswordController::class, 'update'])->name('update');
// パスワード更新終了ページ
Route::get('/edited', [PasswordController::class, 'edited'])->name('edited');
});
これでパスワードリセット関連のルーティングの設定完了です。
パスワードリセットメール送信フォームページの作成
ログインページにパスワードリセットメール送信フォームページへのリンクを設置します。
<a href="{{ route('password_reset.email.form') }}">パスワードをお忘れの方</a>
次に、コントローラを作成します。
php artisan make:controller PasswordController
生成されたPasswordController.php
を下記のように編集します。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class PasswordController extends Controller
{
/**
* パスワード再設定メール送信フォームページ
*
* @return \Illuminate\Contracts\View\View
*/
public function emailFormResetPassword()
{
return view('user.reset_password.email_form');
}
}
次に、パスワード再設定メール送信フォームのビューを作成します。
resources/views/user/reset_password
配下にemail_form.blade.php
を作成し、下記のようなメールアドレスの入力フォームを設置します。
@extends('layouts.user')
@section('page-title')
パスワード再設定メール送信フォーム
@endsection
@section('page-content')
<div>
<h1>パスワード再設定メール送信フォーム</h1>
<form method="POST" action="{{ route('password_reset.email.send') }}">
@csrf
<div>
<label for="email">メールアドレス</label>
<input type="text" name="email" id="email" value="{{ old('email') }}">
@error('email')
<span class="error">{{ $message }}</span>
@enderror
</div>
<button>再設定用メールを送信</button>
</form>
<a href="{{ route('login') }}">戻る</a>
</div>
@endsection
※@extends
、@section
などはご自身の環境に合わせて変更してください。
これで、パスワード再設定メール送信フォームが作成できました。
メール送信処理の実装
メール送信処理を実装していきます。
まず、先ほどのパスワード再設定メール送信フォームで入力されたメールアドレスに対してのバリデーションを設定します。
下記のコマンドを入力し、バリデーションを設定するRequest
ファイルを作成します。
php artisan make:request SendEmailRequest
生成されたSendEmailRequest.php
を下記のように編集します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendEmailRequest 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 [
'email' => 'required|email:filter|exists:users,email'
];
}
/**
* バリデーションメッセージのカスタマイズ
* @return array
*/
public function messages()
{
return [
'email.required' => ':attributeを入力してください',
'email.email' => '正しいメールアドレスの形式で入力してください',
'email.exists' => '登録している:attributeを入力してください'
];
}
/**
* attribute名をカスタマイズ
* @return array
*/
public function attributes()
{
return [
'email' => 'メールアドレス',
];
}
}
設定したバリデーションについて解説します。
email:filter
とすることで平仮名、カタカナ、漢字の入力を防ぐことができます。
詳細は以前書いたこちらの記事をご覧ください。
email:filter
について詳しく解説しています。

exists:users,email
は、入力されたメールアドレスがusersテーブルに登録されているメールアドレスかを確認します。
入力されたメールアドレスが、users
テーブルに登録されていない場合、バリデーションで弾きます。
次に、PasswordController
にsendEmailResetPassword
アクションを追加します。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
##ここから追加##
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Repositories\Interfaces\UserTokenRepositoryInterface;
use App\Http\Requests\SendEmailRequest;
use App\Mail\UserResetPasswordMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Exception;
##ここまで追加##
class PasswordController extends Controller
{
##ここから追加##
private $userRepository;
private $userTokenRepository;
private const MAIL_SENDED_SESSION_KEY = 'user_reset_password_mail_sended_action';
public function __construct(
UserRepositoryInterface $userRepository,
UserTokenRepositoryInterface $userTokenRepository,
)
{
$this->userRepository = $userRepository;
$this->userTokenRepository = $userTokenRepository;
}
##ここまで追加##
/**
* ユーザーのパスワード再設定メール送信フォームページ
*
* @return \Illuminate\Contracts\View\View
*/
public function emailFormResetPassword()
{
return view('user.reset_password.email_form');
}
##ここから追加##
/**
* ユーザーのパスワード再設定メール送信
*
* @param SendEmailRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function sendEmailResetPassword(SendEmailRequest $request)
{
try {
$user = $this->userRepository->findFromEmail($request->email);
$userToken = $this->userTokenRepository->updateOrCreateUserToken($user->id);
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信します。');
Mail::send(new UserResetPasswordMail($user, $userToken));
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信しました。');
} catch(Exception $e) {
Log::error(__METHOD__ . '...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = ' . $request->email . ' error_message = ' . $e);
return redirect()->route('user.password_reset.email_form')
->with('flash_message', '処理に失敗しました。時間をおいて再度お試しください。');
}
// メール送信完了画面への不正アクセスを防ぐためのセッションキー
session()->put(self::MAIL_SENDED_SESSION_KEY, 'user_reset_password_send_email');
return redirect()->route('password_reset.email.send_complete');
}
##ここまで追加##
}
Log::info
, Log::errror
と書いておくことでstorage/logs/laravel.log
にログが出力されるようになります。
メール送信に成功した場合
[2021-12-05 18:22:29] local.INFO: App\Http\Controllers\PasswordController::sendEmailResetPassword...ID:1のユーザーにパスワード再設定用メールを送信します。
[2021-12-05 18:22:29] local.INFO: App\Http\Controllers\PasswordController::sendEmailResetPassword...ID:1のユーザーにパスワード再設定用メールを送信しました。
メール送信に失敗した場合
[2021-12-05 18:20:10] local.INFO: App\Http\Controllers\PasswordController::sendEmailResetPassword...ID:1のユーザーにパスワード再設定用メールを送信します。
[2021-12-05 18:20:11] local.ERROR: App\Http\Controllers\PasswordController::sendEmailResetPassword...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = test@example.com error_message = エラーメッセージが表示されます
特にエラーになった際に、原因を特定しやすくなるので必ずログ出力されるようにしておきしょう!
追加したsendEmailResetPassword
アクションでリポジトリパターン(Interface
とRepository
)が登場しました。
今回は本題ではないためリポジトリパターンの詳しい説明は割愛しますが、登録の手順は説明しますのでご安心ください。
リポジトリパターンの実装
まずは、Interface
とRepository
をbind
する(紐づける)ためのプロバイダを作成します。
php artisan make:provider RepositoryServiceProvider
生成されたRepositoryServiceProvider.php
を下記のように編集します。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Binding of models
*
* @var array
*/
private $models = [
'User',
'UserToken'
];
/**
* Register services.
*
* @return void
*/
public function register()
{
foreach ($this->models as $model) {
$this->app->bind(
"App\Repositories\Interfaces\\{$model}RepositoryInterface",
"App\Repositories\Eloquents\\{$model}Repository"
);
}
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
上記のように記述することで、今後Interface
とRepository
が増えても、$models
の配列の中にモデル名を追記するだけで、bind
(紐づけること)できます。
次に、作成したRepositoryServiceProvider
を登録します。
config/app.php
を開き、下記を追記します。
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
##中略##
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
##下記を追加##
App\Providers\RepositoryServiceProvider::class,
],
これで、RepositoryServiceProvider
がプロバイダとして登録され、リポジトリパターンが機能するようになりました。
では、Interface
とRipository
を作成します。
まずはInterface
からです。
App\Repositories\Interfaces
というディレクトリを作成し、作成したInterfaces
ディレクトリ内にUserRepositoryInterface.php
を作成し、下記のように編集します。
<?php
namespace App\Repositories\Interfaces;
use App\Models\User;
interface UserRepositoryInterface
{
/**
* 引数に渡されたメールアドレスを持つユーザーを取得する
*
* @param string $email
* @return User
*/
public function findFromEmail(string $email): User;
}
次に、Ripository
の実装です。
App\Repositories\Eloquents
ディレクトリを作成し、作成したEloquents
ディレクトリ内にUserRepository.php
を作成し、下記のように編集します。
<?php
namespace App\Repositories\Eloquents;
use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface
{
private $user;
/**
* constructor
*
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @inheritDoc
*/
public function findFromEmail(string $email): User
{
return $this->user->where('email', $email)->firstOrFail();
}
}
findFromEmail
関数では引数に渡されたメールアドレスを持つUser
のインスタンスを返却しています。
これでUser
モデルのリポジトリパターンの実装が終わりました。
次にUserToken
モデルのリポジトリパターンの実装です。
Interfaces
ディレクトリ内にUserTokenRepositoryInterface.php
を作成し、下記のように編集します。
<?php
namespace App\Repositories\Interfaces;
use App\Models\UserToken;
interface UserTokenRepositoryInterface
{
/**
* Userのパスワードリセット用のトークンを発行する
* すでに存在していれば更新する
*
* @param int $userId
* @return UserToken
*/
public function updateOrCreateUserToken(int $userId): UserToken;
}
Eloquents
ディレクトリ内にUserTokenRepository.php
を作成し、下記のように編集します。
<?php
namespace App\Repositories\Eloquents;
use App\Models\UserToken;
use App\Repositories\Interfaces\UserTokenRepositoryInterface;
use Carbon\Carbon;
class UserTokenRepository implements UserTokenRepositoryInterface
{
private $userToken;
/**
* constructor
*
* @param UserToken $userToken
*/
public function __construct(UserToken $userToken)
{
$this->userToken = $userToken;
}
/**
* @inheritDoc
*/
public function updateOrCreateUserToken(int $userId): UserToken
{
$now = Carbon::now();
$hashedToken = hash('sha256', $userId);
return $this->userToken->updateOrCreate(
[
'user_id' => $userId,
],
[
'token' => uniqid(rand(), $hashedToken),
'expire_at' => $now->addHours(48)->toDateTimeString(),
]);
}
}
updateOrCreate
は第一引数に与えられた$userId
がuser_token
テーブルに存在しない場合は新規で作成され、存在する場合はそのレコードを更新してくれるとても便利なメソッドです。(参考:Laravel 8.x Eloquentの準備)
$hashedToken = hash('sha256', $userId)
は$userId
をハッシュ化し、変数に代入しています。(参考:PHPでハッシュ値(SHA1/SHA2/SHA3/MD5)を生成する)
uniqid(rand(), $hashedToken)
でハッシュ化した$userId
の文字列を含むトークンを作成しています。(参考:ユニークなIDを生成!PHPでuniqidを使う方法)
'expire_at' => $now->addHours(48)->toDateTimeString()
でトークンの有効期限を現在から48時間後としています。
パスワードリセットメールの作成
次にメールを作成します。
PasswordController
のsendEmailResetPassword
アクションの下記の部分です。
Mail::send(new UserResetPasswordMail($user, $userToken));
下記のコマンドを入力します。
php artisan make:mail UserResetPasswordMail
生成されたUserResetPasswordMail.php
を下記のように編集します。
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
Use App\Models\UserToken;
use Carbon\Carbon;
use Illuminate\Support\Facades\URL;
class UserResetPasswordMail extends Mailable
{
use Queueable, SerializesModels;
private $user;
private $userToken;
/**
* コンストラクト
*
* @param User $user
* @param UserToken $userToken
*/
public function __construct(
User $user,
UserToken $userToken
)
{
$this->user = $user;
$this->userToken = $userToken;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$tokenParam = ['reset_token' => $this->userToken->token];
$now = Carbon::now();
// 48時間後を期限とした署名付きURLを生成
$url = URL::temporarySignedRoute('password_reset.edit', $now->addHours(48), $tokenParam);
return $this->from('送信元のメールアドレス', '送信元の名前')
->to($this->user->email)
->subject('パスワードをリセットする')
->view('mails.password_reset_mail')
->with([
'user' => $this->user,
'url' => $url,
]);
}
}
送信元のメールアドレスと送信元の名前はご自身の環境のものを設定してください。
次に、メールのビューを作成します。
resources/views/mails/
配下にpassword_reset_mail.blade.php
を作成し、メールの文言を設定してください。(下記は一例です。ご参考にどうぞ。)
※このメールはパスワードの再設定をご希望された方にお送りしております。<br>
<br>
<p>{{ $user->name }}様</p><br>
いつも{{ config('app.name') }}をご利用いただき、誠にありがとうございます。<br>
パスワード再設定用のURLをお送りします。<br>
<br>
<a href="{{ $url }}">{{ $url }}</a><br>
<br>
上記URLにアクセスし、パスワードの再設定を行ってください。<br>
有効期限は本メールを受信してから48時間となります。<br>
<br>
<br>
※※※※※本メールは送信専用のメールアドレスから送信しております。ご返信できませんのでご了承ください。※※※※※<br>
<br>
<br>
<br>
<br>
<br>
<p>
---------------------------------<br>
{{ config('app.name') }}運営事務局<br>
<br>
〒123-4567<br>
東京都新宿区南新宿11-22-33<br>
TEL:03-1234-xxxx<br>
---------------------------------
</p>
これでパスワードリセットメール送信処理の実装が完了です。
パスワードリセットメール送信完了ページの作成
パスワードリセットメール送信完了ページを作成していきます。
PasswordController.php
にsendComplete
アクションを追加します。
/**
* ユーザーのパスワードリセットメール送信完了ページ
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function sendComplete()
{
// メール送信処理で保存したセッションキーに値がなければアクセスできないようにすることで不正アクセスを防ぐ
if (session()->pull(self::MAIL_SENDED_SESSION_KEY) !== 'user_reset_password_send_email') {
return redirect()->route('password_reset.email.form')
->with('flash_message', '不正なリクエストです。');
}
return view('user.reset_password.send_complete');
}
ビューを作成します。
resources/views/user/reset_password
配下にsend_complete.blade.php
を作成し、パスワードリセットメール送信が完了した旨を伝える文言を設定してください。(下記は一例です。)
@extends('layouts.user')
@section('page-title')
パスワードリセットメール送信完了
@endsection
@section('page-content')
<div>
<h1>パスワードリセットメールを送信しました。</h1>
<a href="{{ route('login') }}">TOPへ</a>
</div>
@endsection
新パスワード入力フォームページの作成
新パスワード設定ページを作成していきます。
PasswordController.php
にedit
アクションを追加します。
/**
* ユーザーのパスワード再設定フォーム画面
*
* @param Request $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function edit(Request $request)
{
if (!$request->hasValidSignature()) {
abort(403, 'URLの有効期限が過ぎたためエラーが発生しました。パスワードリセットメールを再発行してください。');
}
$resetToken = $request->reset_token;
try {
$userToken = $this->userTokenRepository->getUserTokenfromToken($resetToken);
} catch (Exception $e) {
Log::error(__METHOD__ . ' UserTokenの取得に失敗しました。 error_message = ' . $e);
return redirect()->route('password_reset.email.form')
->with('flash_message', __('パスワードリセットメールに添付されたURLから遷移してください。'));
}
return view('user.reset_password.edit')
->with('userToken', $userToken);
}
userTokenRepository
のgetUserTokenfromToken
関数を実装します。
UserTokenRepositoryInterface.php
に下記を追加します。
<?php
namespace App\Repositories\Interfaces;
use App\Models\UserToken;
interface UserTokenRepositoryInterface
{
/**
* Userのパスワードリセット用のトークンを発行する
* すでに存在していれば更新する
*
* @param int $userId
* @return UserToken
*/
public function updateOrCreateUserToken(int $userId): UserToken;
##ここから追加##
/**
* トークンからUserTokenのレコードを1件取得
*
* @param string $token
* @return UserToken
*/
public function getUserTokenfromToken(string $token): UserToken;
}
次に、実装クラスのUserTokenRepository.php
に下記を追加します。
<?php
namespace App\Repositories\Eloquents;
use App\Models\UserToken;
use App\Repositories\Interfaces\UserTokenRepositoryInterface;
use Carbon\Carbon;
class UserTokenRepository implements UserTokenRepositoryInterface
{
private $userToken;
/**
* constructor
*
* @param UserToken $userToken
*/
public function __construct(UserToken $userToken)
{
$this->userToken = $userToken;
}
/**
* @inheritDoc
*/
public function updateOrCreateUserToken(int $userId): UserToken
{
$now = Carbon::now();
$provitionalToken = hash('sha256', $userId, '');
return $this->userToken->updateOrCreate(
[
'user_id' => $userId,
],
[
'token' => uniqid(rand(), $provitionalToken),
'expire_at' => $now->addHours(48)->toDateTimeString(),
]);
}
##ここから追加##
/**
* @inheritDoc
*/
public function getUserTokenfromToken(string $token): UserToken
{
return $this->userToken->where('token', $token)->firstOrFail();
}
}
これでgetUserTokenfromToken
関数の実装は終わりです。
次に新パスワード入力フォームページのビューを作成します。
resources/views/user/reset_password
配下にedit.blade.php
を作成し、下記のようにパスワードの入力フォームを設置します。
@extends('layouts.user')
@section('page-title')
新パスワード入力フォーム
@endsection
@section('page-content')
<div>
<h1 class="title">新しいパスワードを設定</h1>
<form method="POST" action="{{ route('password_reset.update') }}">
@csrf
<input type="hidden" name="reset_token" value="{{ $userToken->token }}">
<div class="input-group">
<label for="password" class="label">パスワード</label>
<input type="password" name="password" class="input {{ $errors->has('password') ? 'incorrect' : '' }}">
@error('password')
<div class="error">{{ $message }}</div>
@enderror
@error('token')
<div class="error">{{ $message }}</div>
@enderror
</div>
<div class="input-group">
<label for="password_confirmation" class="label">パスワードを再入力</label>
<input type="password" name="password_confirmation" class="input {{ $errors->has('password_confirmation') ? 'incorrect' : '' }}">
</div>
<button type="submit">パスワードを再設定</button>
</form>
</div>
@endsection
これでユーザーの新パスワード入力フォームページの作成が完了です。
パスワード更新処理の実装
パスワード更新処理の実装をしていきます。
バリデーションの設定
まずはパスワードのバリデーションを設定するために、Request
クラスを作成します。
php artisan make:request ResetPasswordRequest
生成されたResetPasswordRequest.php
を下記のように編集します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Rules\TokenExpirationTimeRule;
class ResetPasswordRequest 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 [
'password' => ['required', 'regex:/^[0-9a-zA-z-_]{8,32}$/', 'confirmed'],
'password_confirmation' => ['required', 'same:password'],
'reset_token' => ['required', new TokenExpirationTimeRule],
];
}
/**
* バリデーションメッセージのカスタマイズ
* @return array
*/
public function messages()
{
return [
'password.required' => ':attributeを入力してください',
'password.regex' => ':attributeは半角英数字とハイフンとアンダーバーのみで8文字以上32文字以内で入力してください',
'password.confirmed' => ':attributeが再入力欄と一致していません',
];
}
/**
* attribute名をカスタマイズ
* @return array
*/
public function attributes()
{
return [
'password' => 'パスワード',
];
}
}
regex:/^[0-9a-zA-z-_]{8,32}$
の正規表現は、半角英数字と-
と_
のみで8文字以上32文字以内で入力されているかをチェックしています。confirmed
で際入力欄と一致しているかをチェックしています。
次にreset_token
のバリデーションに設定されているカスタムバリデーションのTokenExpirationTimeRule
を作成するために、下記のコマンドを入力します。
php artisan make:rule TokenExpirationTimeRule
生成されたTokenExpirationTimeRule.php
を下記のように編集します。
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Carbon\Carbon;
use App\Repositories\Interfaces\UserTokenRepositoryInterface;
class TokenExpirationTimeRule implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
}
/**
* トークンの有効期限が切れていないかチェックする
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
$now = Carbon::now();
$userTokenRepository = app()->make(UserTokenRepositoryInterface::class);
$userToken = $userTokenRepository->getUserTokenfromToken($value);
$expireTime = new Carbon($userToken->expire_at);
return $now->lte($expireTime);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return '有効期限が過ぎています。パスワードリセットメールを再発行してください。';
}
}
このTokenExpirationTimeRule
でトークンの有効期限がきれていないかをチェックしています。
以上でバリデーションができました。
PasswordControllerにupdateアクションを追加
PasswordController.php
にupdate
アクションを追加します。
下記を追加してください。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Requests\ResetPasswordRequest;
##下記のResetPasswordRequestを追加##
use App\Http\Requests\ResetPasswordRequest;
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Repositories\Interfaces\UserTokenRepositoryInterface;
use App\Http\Requests\SendEmailRequest;
use App\Mail\UserResetPasswordMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Exception;
class PasswordController extends Controller
{
private $userRepository;
private $userTokenRepository;
private const MAIL_SENDED_SESSION_KEY = 'user_reset_password_mail_sended_action';
##下記のプライベート定数を追加##
private const UPDATE_PASSWORD_SESSION_KEY = 'user_update_password_action';
public function __construct(
UserRepositoryInterface $userRepository,
UserTokenRepositoryInterface $userTokenRepository,
)
{
$this->userRepository = $userRepository;
$this->userTokenRepository = $userTokenRepository;
}
##中略##
##ここから追加##
/**
* パスワード更新処理
*
* @param ResetPasswordRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function update(ResetPasswordRequest $request)
{
try {
$userToken = $this->userTokenRepository->getUserTokenfromToken($request->reset_token);
$this->userRepository->updateUserPassword($request->password, $userToken->user_id);
Log::info(__METHOD__ . '...ID:' . $userToken->user_id . 'のユーザーのパスワードを更新しました。');
} catch (Exception $e) {
Log::error(__METHOD__ . '...ユーザーのパスワードの更新に失敗しました。...error_message = ' . $e);
return redirect()->route('password_reset.email_form')
->with('flash_message', __('処理に失敗しました。時間をおいて再度お試しください。'));
}
// パスワードリセット完了画面への不正アクセスを防ぐためのセッションキー
$request->session()->put(self::UPDATE_PASSWORD_SESSION_KEY, 'user_update_password');
return redirect()->route('password_reset.edited');
}
}
userRepository
の新しい関数updateUserPassword
が出てきましたので、実装していきます。
UserRepositoryInterface.php
に下記を追加します。
<?php
namespace App\Repositories\Interfaces;
use App\Models\User;
interface UserRepositoryInterface
{
/**
* 引数に渡されたメールアドレスを持つユーザーを取得する
*
* @param string $email
* @return User
*/
public function findFromEmail(string $email): User;
##ここから追加##
/**
* 引数に渡されたIDのユーザーのパスワードを更新する
*
* @param string $password
* @param int $id
* @return void
*/
public function updateUserPassword(string $password, int $id): void;
}
実装クラスのUserRepository.php
にも下記を追加します。
<?php
namespace App\Repositories\Eloquents;
use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
##下記のuseを追加##
use Illuminate\Support\Facades\Hash;
class UserRepository implements UserRepositoryInterface
{
private $user;
/**
* constructor
*
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @inheritDoc
*/
public function findFromEmail(string $email): User
{
return $this->user->where('email', $email)->firstOrFail();
}
##ここから追加##
/**
* @inheritDoc
*/
public function updateUserPassword(string $password, int $id): void
{
$this->user->where('id', $id)->update(['password' => Hash::make($password)]);
}
}
このupdateUserPassword
でユーザーのパスワードの更新処理を行っています。
以上でパスワード更新処理の実装が終わりました。
いよいよ次が最後のパートです。
パスワード再設定完了ページの作成
パスワード再設定完了ページを作成します。
まずは、PasswordController
にedited
アクションを追加します。
/**
* パスワードリセット完了ページ
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function edited()
{
// パスワード更新処理で保存したセッションキーに値がなければアクセスできないようにすることで不正アクセスを防ぐ
if (session()->pull(self::UPDATE_PASSWORD_SESSION_KEY) !== 'user_update_password') {
return redirect()->route('password_reset.email.form')
->with('flash_message', '不正なリクエストです。');
}
return view('user.reset_password.edited');
}
最後にビューファイルを作成します。
resources/views/user/reset_password
配下にedited.blade.php
を作成し、パスワードリセットが完了した旨を伝える文言を表示してください。(下記は一例です。)
@extends('layouts.user')
@section('page-title')
パスワード再設定完了
@endsection
@section('page-content')
<div>
<h1>パスワードリセットが完了しました</h1>
<a href="{{ route('login') }}">TOPへ</a>
</div>
@endsection
以上でパスワードリセット機能の実装が全て完了しました。
実装お疲れ様でした!
さいごに
いかがでしたでしょうか。
パスワードリセット機能の実装方法について理解できましたでしょうか?
ご質問やご指摘等ある方はコメントしていただければ返信させていただきます。
これからもLaravelエンジニアの方、Laravel学習中の初学者の方に役に立つ記事を更新していきますのでよろしくお願いします!
Twitterでも情報発信をしていますので、よかったらフォローをお願いします(^^)
お疲れ様です。
laravel学習中の初学者なのですが、リポジトリパターン作成の時の
“`
Eloquentsディレクトリ内にUserRepository.phpを作成し、下記のように編集します。
“`
は同じ名前のものが二つになるので、もしかしたら「UserTokenRepository」ではないでしょうか?
それからコピペして気づいたのですが、UserTokenRepositoryの内容の最後に「}」が足りない気がします。
あと質問なのですが、「パスワードリセットメールの作成」画面の「’送信元のメールアドレス’, ‘送信元の名前’」部分は、どの様に記載すればメールアドレスと名前を指定できますか?初歩的な質問ですみません。
タカタカさん
返信遅くなってしまい大変申し訳ございません。
> もしかしたら「UserTokenRepository」ではないでしょうか?
それからコピペして気づいたのですが、UserTokenRepositoryの内容の最後に「}」が足りない気がします。
ご指摘いただきありがとうございます。
おっしゃる通りでしたので修正しました。
> 「パスワードリセットメールの作成」画面の「’送信元のメールアドレス’, ‘送信元の名前’」部分は、どの様に記載すればメールアドレスと名前を指定できますか?
return $this->from(‘hogefuga@example.com’, ‘山田太郎’)
のように直書きしても良いのですが、
メールアドレスなどの重要な情報はconfigに定義してから呼び出したりします。
以下のような形です。
return $this->from(config(‘mail.from.address’), config(‘mail.from.name’))
config/mail.phpに以下のように定義
‘from’ => [
‘address’ => ‘hogefuga@example.com’,
‘name’ => ‘山田太郎’
],
configについてわからない場合は以前 https://takuma-it.com/laravel-config/ の記事で解説しているのでご参考になれば幸いです。
初めてlaravelアプリを作る中で、タクマさんがお作りになったものを参考に、自分のアプリに導入させていただきました。
XAMPを使って開発環境では、メールも送れてパスワードの変更もできておりました。
これを、VPS、CentOSの公開環境にデプロイして、パスワードの再設定を試したところ、何度やってもメールの送信に失敗します。メールはユーザー登録時に送るようにしていますが、こちらは問題なく送れます。
色々探ってはみましたが、どうしてもわからず、ここはタクマさんにお知恵を拝借するしかないと思い、ご連絡した次第です。つまらない質問かもとは思いますが、ご教授のほどよろしくお願いします。
I love laravelさん
返信遅くなってしまい申し訳ありません。
すみません、I love laravelさんの環境を再現できるわけではないので原因は私も分かりかねますm(_ _)m
エラーログは出ていませんでしょうか?
ご回答ありがとうございます。
エラーログは、以下となります。
これを読むと、接続できないようですが、どこに誤りがあるのかわかりません。初めに申しましたようにユーザー登録時に送られるメールは遅れています。その場合は、JOBを使っています。
これと同じく、ForgotPasswordもMail::send()を使っていますが、こちらも送れません。
環境は、VPS、CentOS Stream9 Apache2.4 php8.2 laravel9です。
production.ERROR: App\Http\Controllers\User\PasswordController::sendEmailResetPassword…ユーザーへのパスワード再設定用メール送信に失敗し ました。 request_email = 送信先メールアドレス error_message = Symfony\Component\Mailer\Exception\TransportException: Connection could not be established with host “ssl://メールサーバーホスト名:465”: stream_socket_client(): Unable to connect to ssl:メールサーバーホスト名:465 (Permission denied) in /var/www/OkawaProject/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php:154
I love laravelさん
返信遅くなってしまい申し訳ありません。
メールドライバの設定はしていますでしょうか?
していないようでしたら https://readouble.com/laravel/8.x/ja/mail.html を参考にお使いのドライバの設定をしてみてください。
もしくは、このパスワードリセットメールもユーザー登録時のメール送信と同じようにJobを使ってみてはいかがでしょうか?
いろいろ調べていったところ、SELinuxに原因がありました。あまり理解しておらず、Enfocingモードになっていましたので、SELinuxからけられていました。Permissiveモードへ変更し、問題個所を設定しないしたところ、再度Enforcingモードに戻してもOKでした。
タクマさんがつくられたすばらい機能を活用させていただくことができるようになってうれしいです。
セキュリティ面で感動しました。
リセットをリクエストしたPCとブラウザが同じでないと、設定画面が表示されないのはgood job!です。
今回のトラブルで、私の知識もぐんと飛躍することができました。本当にありがとうございました。
さいごに、お気づきとは思いますが、ニックネームのlalavelはrとlの間違いでして、笑ってやってください。
I love laravelさん
解決されたようでよかったです!
そう言っていただけると私も嬉しいです!ありがとうございます!
> さいごに、お気づきとは思いますが、ニックネームのlalavelはrとlの間違いでして、笑ってやってください。
全然気づいていませんでした。笑