【Laravel】パスワードリセット機能の実装方法!カスタマイズ

この記事からわかること
- Laravelでパスワードリセット機能を開発する流れ
- メールの送信方法
- カスタマイズや日本語化するには?
index
[open]
\ アプリをリリースしました /
環境
- Laravel:8.83.29
- PHP:8.2.0
- macOS:Sonoma 14.6.1
パスワードリセット機能
Laravelでは認証機能を比較的簡単に実装することが可能です。それに伴いユーザーがパスワードを忘れてログインできなくなった場合の救済措置として必要な「パスワードリセット機能」の簡単に実装できるようになっています。
今回はLaravel8系においてパスワードリセット機能の実装方法をまとめていきます。また前提として認証機能の実装が完了していることが条件になるので未実装の方は以下の記事を参考にしてください。
実装手順
- password_resetsテーブルの作成
- パスワードリセットメール送信処理の実装
- リセット処理の実装
- パスワードリセット入力画面と結果画面のViewを実装
- ルーティングの定義
- .envにメール設定
1.password_resetsテーブルの作成
パスワードリセット機能を使用するためには対象のメールアドレスと発行用のトークンを管理するpassword_resets
テーブルを作成しておく必要があります。このテーブルのマイグレーションファイルはLaravelがデフォルトで用意してくれているのでphp artisan migrate
を実行するだけでOKです。
$ php artisan migrate
2.パスワードリセットメール送信処理の実装
続いてパスワードリセットメール送信処理の実装をしていきます。PasswordResetController
(名前は何でも良い)を作成してその中に諸々の処理を記述していきます。
sendResetLinkEmail
メソッドを定義しRequest
で対象のメールアドレスを受け取れるようにしておきます。ここのバリデーション時点でUser
Eloquentに対象のメールアドレスがそもそも存在するかをチェックしています。
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class PasswordResetController extends Controller
{
// パスワードリセットメールを送信
public function sendResetLinkEmail(Request $request)
{
// 対象のメールアドレスがそもそも存在するかをチェック
$validator = Validator::make($request->all(), [
'email' => 'required|email|exists:users,email',
]);
if ($validator->fails()) {
return response()->json(['error' => '対象のメールアドレスのユーザーが存在しません。'], 400);
}
// メールを送信する
$status = Password::sendResetLink($request->only('email'));
return $status === Password::RESET_LINK_SENT
? response()->json(['message' => 'パスワードリセットリンクメールを送信しました。'], 200)
: response()->json(['error' => 'パスワードリセットリンクメールの送信に失敗しました。'], 400);
}
}
実際にメールを送信しているのはPassword::sendResetLink
部分です。Laravelではこの実装だけで決まったテンプレートのパスワードリセットメールを送信してくれます。成功/失敗のステータスでレスポンスだけハンドリングしておきます。
実際に以下のようなデザインのメールが届きます。ユーザーはこのボタンをクリックしてパスワードリセット処理を進める流れになります。

またこのリンクをタップした際に表示するViewは用意されないので自前で用意する必要があります。これは後述しています。
3.リセット処理の実装
続いて実際にパスワードをリセットする処理を実装していきます。reset
メソッドを定義しRequest
では「トークン」、「メールアドレス」、「変更先のパスワード」を受け取ります。
パスワードのリセット処理はPassword::reset
で行います。このメソッドでは内部的に以下のことを実行してくれます。
- メールアドレスとリセットトークンを検証
- usersテーブルのユーザーを取得
- $callback を実行して、新パスワードに更新
- password_resets テーブルから該当するトークンを削除
- Password::PASSWORD_RESET / INVALID_TOKENを返す
// パスワードをリセット
public function reset(Request $request)
{
$validator = Validator::make($request->all(), [
'token' => 'required',
'email' => 'required|email|exists:users,email',
'password' => 'required|min:8|confirmed',
]);
if ($validator->fails()) {
return redirect()->route('password.reset.result')->with([
'status' => 'error',
'message' => '入力内容に誤りがあります。',
'errors' => $validator->errors()
]);
}
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
// User情報にパスワードをハッシュ化して保存し直す
$user->forceFill([
'password' => Hash::make($password),
])->save();
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('password.reset.result')->with([
'status' => 'success',
'message' => 'パスワードが正常にリセットされました。'
])
: redirect()->route('password.reset.result')->with([
'status' => 'error',
'message' => 'パスワードのリセットに失敗しました。'
]);
}
パスワードリセットが成功 / 失敗した場合は結果画面Viewへリダイレクトさせます。ここの画面UIの構築も次で作成します。
4.パスワードリセット入力画面と結果画面のViewを実装
これでロジック部分の実装は完了したのであとはViewとルーティングを定義します。resources/views/auth/passwords
配下にblade
ファイルを用意していきます。ここは特に解説はしません。
@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">{{ __('パスワードリセット') }}</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.update') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label for="email">{{ __('メールアドレス') }}</label>
<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 class="form-group">
<label for="password">{{ __('新しいパスワード') }}</label>
<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 class="form-group">
<label for="password-confirm">{{ __('パスワード(確認)') }}</label>
<input id="password-confirm" type="password" class="form-control"
name="password_confirmation" required autocomplete="new-password">
</div>
<div class="form-group mt-3">
<button type="submit" class="btn btn-primary">
{{ __('パスワードをリセット') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@extends('layouts.app')
@section('content')
<div class="container">
@if (session('status') === 'success')
<div class="alert alert-success">
<h4>パスワードリセット成功</h4>
<p>{{ session('message') }}</p>
<p>新しいパスワードでログインしてください。</p>
</div>
@else
<div class="alert alert-danger">
<h4>パスワードリセット失敗</h4>
<p>{{ session('message') }}</p>
@if(session('errors'))
<ul>
@foreach(session('errors')->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<p>お手数ですが時間をあけてから再度お試しください。</p>
@endif
</div>
@endif
</div>
@endsection
CSSはBootstrap
を使用してチートしておきます。
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
5.ルーティングの定義
これでロジックとViewの定義が完了したのルーティングを定義していきます。定義しなければいけないのは以下の4つです。
- パスワードリセットメール送信処理ポイント
- パスワードリセット用画面ルート
- パスワード更新処理ポイント
- パスワードリセット結果画面ルート
パスワードリセットメール送信処理はAPIで実行される想定のためapi.php
に定義しておきます。
// パスワードリセットリクエスト
Route::post('/password/email', [PasswordResetController::class, 'sendResetLinkEmail']);
残りはweb.php
に定義しておきます。
// パスワードリセット用画面
Route::get('/reset-password/{token}', function ($token) {
return view('auth.passwords.reset', ['token' => $token]);
})->middleware('guest')->name('password.reset');
// パスワード更新処理
Route::post('/password/reset', [PasswordResetController::class, 'reset'])->name('password.update');
// パスワードリセット結果画面
Route::get('/password/reset/result', [PasswordResetController::class, 'resetResult'])->name('password.reset.result');
6..envにメール設定
最後にメールがちゃんと送信できるように.env
にメール設定を追加しておきます。
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=自分のメールアドレス
MAIL_PASSWORD=アプリパスワード
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=自分のアドレス
MAIL_FROM_NAME="${APP_NAME}"
設定する内容は以下の記事を参考にしてください。
パスワードリセットトークンの有効期限を変更する
パスワードリセットトークンの有効期限を変更
するにはconfig/auth.php
内の以下の部分を変更します。ここで有効期限だけでなく再送可能までのインターバル時間も変更できます。
パスワードリセット
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60, // 有効期限
'throttle' => 60, // 再送可能までのインターバル
],
],
リセットメールのカスタマイズ
パスワードリセットメールの内容をカスタマイズするためには専用のNotification
クラスを用意します。App\Notifications
に,ResetPasswordNotification.php
を作成しtoMail
メソッド部分でカスタマイズしていきます。内容はsubject
で件名をline
などで文を追加することが可能になっています。
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPasswordNotification extends Notification
{
public $token;
public function __construct($token)
{
$this->token = $token;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new MailMessage)
->subject('パスワードリセットのお知らせ')
->line('以下のボタンをクリックして、パスワードをリセットしてください。')
->action('パスワードをリセット', url(config('app.url') . route('password.reset', ['token' => $this->token, 'email' => $notifiable->email], false)))
->line('このメールに心当たりがない場合は、無視してください。');
}
}
作成したらApp\Models
内のUser
でsendPasswordResetNotification
メソッドをオーバーライドして先ほど作成したsendPasswordResetNotification
を使用するように変更しておきます。
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
これでメール件名や内容を自由にカスタマイズしたものを反映させることができます。
ローカライズして日本語対応
上記のように明示的に文字を指定するのではなくローカライズして日本語対応をしたい場合はローカライズ用のファイルを用意して適応させばOKです。resources/lang/ja/passwords.php
を用意して中に以下のように記述します。
return [
'reset_password_notification' => '【' . config('app.name') . '】パスワードリセット通知メールです',
'greeting' => 'パスワードリセット',
'reset_request_received' => 'アカウントのパスワードリセットリクエストを受け付けました。以下のボタンからパスワードをリセットしてください。',
'reset_password' => 'パスワードリセット',
'reset_link_expire' => 'このパスワードリセットリンクは、:count 分で期限切れになります。',
'no_action_required' => 'このメールに身に覚えがない場合は無視してください。',
'trouble_clicking' => '":actionText"ボタンをクリックできない場合は、以下のURLをコピーしてブラウザに貼り付けてください。',
];
あとはtoMail
メソッドを以下のように修正すればローカライズしたテキストが反映されるようになります。
public function toMail($notifiable)
{
return (new MailMessage)
->subject(Lang::get('passwords.reset_password_notification'))
->greeting(Lang::get('passwords.greeting'))
->line(Lang::get('passwords.reset_request_received'))
->action(
Lang::get('passwords.reset_password'),
url(config('app.url') . route('password.reset', [
'token' => $this->token,
'email' => $notifiable->email
], false))
)
->line(Lang::get('passwords.reset_link_expire', [
'count' => Config::get('auth.passwords.' . Config::get('auth.defaults.passwords') . '.expire')
]))
->line(Lang::get('passwords.no_action_required'))
->line(Lang::get('passwords.trouble_clicking', [
'actionText' => Lang::get('passwords.reset_password')
]));
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。