はじめに
今回は、Laravelでパスワードリセット機能の実装についてまとめました。
※おことわり※
基本的に学習内容のアウトプットです。
初学者であるため、間違い等あればご指摘いただけますと嬉しいです。
この記事の目的
以下内容のアウトプット
- カラムの追加 ・・・マイグレーション機能
- メール送信機能の実装 ・・・Mailableクラス
- パスワードリセット機能を実装 ・・・リポジトリパターン
- パスワードリセット機能を実装 ・・・ファサード
- トークンの有効期限の判定 ・・・オリジナルバリデーション
この記事の内容
- 前提
- 実装内容と大まかな手順
- 既存テーブルにカラムの追加
- ルーティング・コントローラの設定
- ビューの作成
- プロバイダの作成・登録
- バリデーションの実装_part1
- リポジトリパターンの実装_part1
- Mailableクラスの実装
- コントローラにメール送信処理を追加
- バリデーションの実装_part2
- リポジトリパターンの実装_part2
- コントローラにパスワード更新処理を追加
- おまけ:ファサードで実装
- 参考
1. 前提
テーブルについて
- userテーブルが存在している
- userテーブルには下記カラムが存在している
- user_id(ユーザーID)
- mail(メールアドレス)
- password(パスワード)
機能について
- ログイン機能実装済み(UsersController)
2. 実装内容と大まかな手順
以下にて、パスワードリセット機能の内容と、実装の大まかな手順について説明します。
実装内容
- パスワード再設定を希望するユーザーに、パスワード再設定フォームのURLをメールにて送付します。
- URLには、有効期限と署名が付いていて、不正なアクセスを制御します。
- 有効期限内かつ署名検証をパスした場合、ユーザーはパスワードを更新することができます。
大まかな手順
以下の順番で実装します。
実装完了を100%としたら、①20% ②50% ③30% といったボリューム感です。
① パスワードリセット機能実装のための準備
② メール送信の実装
③ パスワード更新の実装
目次
*①パスワードリセット機能実装のための準備
3. 既存テーブルにカラムの追加
4. ルーティング・コントローラの設定
5. ビューの作成
6. プロバイダの作成・登録
*②メール送信処理
7. バリデーションの実装_part1
8. リポジトリパターンの実装_part1
9. Mailableクラスの実装
10. コントローラにメール送信処理を追加
*③パスワード更新処理
11. バリデーションの実装_part2
12. リポジトリパターンの実装_part2
13. コントローラにパスワード更新処理を追加
ここから実装に入っていきます。
はじめに、「①パスワードリセット機能実装のための準備」を行います。
3. 既存テーブルにカラムの追加
userテーブルに、2つのカラムを追加します。
追加の手順については、【Laravel】既存テーブルに好きな数のカラムを追加する を参照ください。
追加するカラムの詳細
Tips:
-
reset_password_access_key
-
nullable
ユーザー新規登録の際、パスワード再設定キーはnullになるためnullを許容にしています。 -
unique
トークンが重複しないよう、ユニーク制約を設定しています。
パスワード再設定キーは、トークンのことです。
-
カラム名 | データ型 | 制約 | コメント |
---|---|---|---|
reset_password_access_key | VARCHAR(64) | nullable, unique | パスワード再設定キー |
reset_password_expire_at | TIMESTAMP | nullable | パスワード再設定キーの有効期限 |
参照
4. ルーティング・コントローラの設定
ルーティングとコントローラの設定を行います。
ルーティングの設定
パスワードリセット関連のルーティングを設定ます。
// 省略
Route::prefix('reset')->group(function () {
// パスワード再設定用のメール送信フォーム
Route::get('/', 'UsersController@requestResetPassword')->name('reset.form');
// メール送信処理
Route::post('/send', 'UsersController@sendResetPasswordMail')->name('reset.send');
// メール送信完了
Route::get('/send/complete', 'UsersController@sendCompleteResetPasswordMail')->name('reset.send.complete');
// パスワード再設定
Route::get('/password/edit', 'UsersController@resetPassword')->name('reset.password.edit');
// パスワード更新
Route::post('/password/update', 'UsersController@updatePassword')->name('reset.password.update');
});
コントローラの設定
コントローラに各ファンクションの定義をします。
新規でコントローラを作成する場合は、Tipsを参照ください。
ビューは次章で作成します。
引数のバリデーション(ResetInputMailRequest, ResetPasswordRequest)は後ほど実装します。
Tips:
- コントローラを新規で作成する場合
php artisan make:controller ディレクトリ/コントローラ名
を実行し、コントローラを作成します。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class UsersController extends Controller
{
/**
* パスワードリセット
*/
// パスワード再設定用のメール送信フォーム
public function requestResetPassword()
{
return view('users.reset_input_mail');
}
// メール送信
public function sendResetPasswordMail(ResetInputMailRequest $request)
{
return redirect()->route('reset.send.complete');
}
// メール送信完了
public function sendCompleteResetPasswordMail()
{
return view('users.reset_input_mail_complete');
}
// パスワード再設定
public function resetPassword(Request $request)
{
return view('users.reset_input_password');
}
// パスワード更新
public function updatePassword(ResetPasswordRequest $request)
{
return view('users.reset_input_password_complete');
}
}
5. ビューの作成
以下の通りビューを作成します。
いずれのビューファイルも
resources/views/users
配下に作成しています。
最低限のコードになりますので、アレンジして使ってください。
ログインページ(既存)
パスワード再設定用のメール送信フォームへのリンクを設置します。
<a href="{{ route('reset.form') }}">パスワードをお忘れの方</a>
メール送信フォーム
@extends('xxxx')
@section('xxxx')
<main>
<h2>パスワード再設定</h2>
<p>ご利用中のメールアドレスを入力してください</p>
<p>パスワード再設定のためのURLをお送りします</p>
<form method="POST" action="{{ route('reset.send') }}">
@csrf
<div>
<label>メールアドレス</label>
<input type="text" name="mail" value="{{ old('mail') }}">
<span>{{ $errors->first('mail') }}</span>
</div>
<div>
<a href="{{ route('xxxx') }}">戻る</a>
<button type="submit">再設定メールを送信</button>
</div>
</form>
</main>
@endsection
メール送信完了ページ
@extends('xxxx')
@section('xxxx')
<main>
<h2>メール送信完了</h2>
<div>
<p>パスワード再設定用のメールを送信しました</p>
<p>メールに記載されているリンクからパスワードの再設定を行ってください</p>
</div>
<div>
<a href="{{ route('xxxx') }}">ログイン画面へ</a>
</div>
</main>
@endsection
パスワード再設定フォーム
@extends('xxxx')
@section('xxxx')
<main>
<h2>パスワード再設定</h2>
<form method="POST" action="{{ route('reset.password.update') }}">
@csrf
<input type="hidden" name="reset_token" value="{{ $userToken->rest_password_access_key }}">
<div>
<label>新パスワード</label>
<input type="password" name="password" value="">
<span>{{ $errors->first('password') }}</span>
<span>{{ $errors->first('reset_token') }}</span>
</div>
<div>
<label>新パスワード<span>確認</span></label>
<input type="password" name="password_confirmation" value="">
</div>
<div>
<button type="submit">パスワードを再設定する</button>
</div>
</form>
</main>
@endsection
パスワード再設定完了ページ
@extends('xxxx')
@section('xxxx')
<main>
<h2>パスワード変更完了</h2>
<div>
<p>パスワードの変更が完了しました</p>
<p>新しいパスワードにて再ログインしてください</p>
</div>
<div>
<a href="{{ route('xxxx') }}">ログイン画面へ</a>
</div>
</main>
@endsection
6. プロバイダの作成・登録
インタフェースとリポジトリを紐づけるためのプロバイダを作成し、設定用ファイルに追加します。
プロバイダの作成
以下コマンドを実行し、プロバイダファイルを作成します。
php artisan make:provider RepositoryServiceProvider
プロバイダの設定
上記で作成したファイルに、以下コードを記述します。
UserRepositoryInterface, UserRepositoryは後ほど実装します。
Tips:
-
$models 配列
今後インタフェースとリポジトリが増えた場合は、配列に追記するだけで bindが行えるように配列を使用しています。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Binding of models
*
* @var array
*/
private $models = [
'User'
];
/**
* 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()
{
//
}
}
プロバイダの登録
上記で作成したプロバイダを、config/app.php
の providers配列
に追加します。
Tips:
- サービスプロバイダクラスとして、提供するサービスが必要な場合にのみ読み込まれます。
'providers' => [
// 中略
/* 下記1行追加 */
App\Providers\RepositoryServiceProvider::class,
],
「①パスワードリセット機能実装のための準備」は以上になります。
続いて「②メール送信の実装」を行います。
7. バリデーションの実装_part1
「パスワード再設定用のメールアドレス入力フォーム用」のバリデーションを実装します。
リクエストファイルの作成
以下コマンドを実行し、リクエストファイルを作成します。
php artisan make:request ResetInputMailRequest
バリデーションの設定
上記で作成したファイルに、バリデーションを定義します。
Tips:
-
email:rfc,dns,filter
-
rfc
RFC準拠になっているか判定します。 -
dnc
メールアドレスのドメインが有効か判定します。 -
filter
平仮名, カタカナ, 漢字の入力を防ぎ、@の後にドットが最低1つ存在するか判定します。
-
-
exists:user,mail
フォームに入力されたメールアドレスが、userテーブルに登録されているか判定します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ResetInputMailRequest 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<string, mixed>
*/
public function rules()
{
return [
'mail' => ['required', 'email:rfc,dns,filter', 'exists:user,mail']
];
}
/**
* エラーメッセージ
* @return array
*/
public function messages()
{
return [
'mail.required' => "メールアドレスを入力してください",,
'mail.email' => "メールアドレスの形式ではありません",
'mail.exists' => "登録しているメールアドレスを入力してください"
];
}
}
8. リポジトリパターンの実装_part1
インタフェースと、リポジトリの作成を行います。
インタフェースとリポジトリは、プロバイダによってbindされます。
ここで定義する処理は、次章で実装するコントローラで呼び出して使います。
インタフェースの作成
以下の通りインタフェースファイルを作成します。
-
App\Repositories
配下にInterfaces
ディレクトリを作成 - 上記で作成したディレクトリ配下に
UserRepositoryInterface.php
を作成
作成したファイルに、以下コードを記述します。
<?php
namespace App\Repositories\Interfaces;
use App\Models\User;
interface UserRepositoryInterface
{
/**
* メールアドレスからユーザー情報を取得
*
* @param string $mail
* @return User
*/
public function findFromMail(string $mail): User;
/**
* パスワードリセット用トークンを発行
*
* @param int $userId
* @return User
*/
public function updateOrCreateUser(int $userId): User;
}
リポジトリの作成
以下の通りリポジトリファイルを作成します。
-
App\Repositories
配下にEloquents
ディレクトリを作成 - 上記で作成したディレクトリ配下に
UserRepository.php
を作成
作成したファイルに、以下コードを記述します。
Tips:
-
updateOrCreateUser(int $userId)
引数の $userIdが userテーブルに存在しない場合、以下2つのカラを新規で作成し、DBに登録します。
存在する場合は、以下2つのカラムを更新します。- rest_password_access_key
- rest_password_expire_data
-
$hashedToken = hash('sha256', $userId);
$userIdをハッシュ化し、変数に代入します。 -
'rest_password_access_key' => uniqid(rand(), $hashedToken)
-
uniqid()
ユニークIDを生成する関数 -
rand()
ランダムな数値を生成する関数
ユニークIDにハッシュ化した値を含ませることで、より一意性を確保することができます。
-
-
'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
トークンの有効期限を、現在から24時間後に設定します。
つまり、現在から24時間後の時間がカラムに登録されます。
<?php
namespace App\Repositories\Eloquents;
use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Carbon\Carbon;
use Illuminate\Support\Facades\Hash;
class UserRepository implements UserRepositoryInterface
{
private $user;
private $userToken;
/**
* constructor
*
* @param User $user
*/
public function __construct(User $user, User $userToken)
{
$this->user = $user;
$this->userToken = $userToken;
}
// メールアドレスからユーザー情報取得
public function findFromMail(string $mail): User
{
return $this->user->where('mail', $mail)->firstOrFail();
}
// パスワードリセット用トークンを発行
public function updateOrCreateUser(int $userId): User
{
$now = Carbon::now();
// $userIdをハッシュ化
$hashedToken = hash('sha256', $userId);
return $this->userToken->updateOrCreate(
[
'id' => $userId,
],
[
// $hashedTokenを含むトークンを作成
'rest_password_access_key' => uniqid(rand(), $hashedToken),
// トークンの有効期限を現在から24時間後に設定
'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
]);
}
}
9. Mailableクラスの実装
Mailableクラスを作成し、パスワード再設定用メールの送信処理を実装します。
Mailableクラスは、コントローラ(メール送信)で呼び出して使用します。
Mailableクラスを作成
以下コマンドを実装し、Mailableクラスを継承したファイルを作成します。
php artisan make:mail ResetPasswordMail
ResetPasswordMailクラスの実装
上記で作成したファイルに、以下コードを記述します。
Tips:
-
URL::temporarySignedRoute
署名付き、有効期限24時間のURLを生成します。 -
URL::temporarySignedRoute
署名付きURLの検証するメソッドです。 -
from(config('mail.from.address'), config('mail.from.name'))
config/mail.phpに以下のように定義します。
送信元の名前はご自身の環境のものを設定してください。
‘from’ => [
‘address’ => ‘hogefuga@example.com’, // 送信元のメールアドレスと
‘name’ => ‘山田太郎’ // 送信元の名前
],
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Carbon\Carbon;
class ResetPasswordMail extends Mailable
{
use Queueable, SerializesModels;
private $user;
private $userToken;
/**
* construct
*
* @param User $user
* @param User $userToken
*/
public function __construct(User $user, User $userToken)
{
$this->user = $user;
$this->userToken = $userToken;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
// トークン取得
$tokenParam = ['reset_token' => $this->userToken->rest_password_access_key];
$now = Carbon::now();
// 署名付き有効期限24時間のURLを生成
$url = URL::temporarySignedRoute('reset.password.edit' , $now->addHours(24), $tokenParam);
// HTML形式でメール作成
return $this->view('users.password_reset_mail')
->subject('パスワード再設定用URLのご案内')
->from(config('mail.from.address'), config('mail.from.name'))
->to($this->user->mail)
->with([
'user' => $this->user,
'url' => $url,
]);
}
}
パスワード再設定用メールの本文を作成
以下の通り、ビューファイルを用意します。
-
resources/views
配下にmails
ディレクトリを作成 - 上記ディレクトリ配下に
password_reset_mail.blade.php
を作成
上記で作成したファイルに、メールの本文を記述します。
最低限のコードになりますので、アレンジして使ってください。
<p>※このメールはパスワード再設定をご希望された方にお送りしております。</p>
<br/>
<p>「xxxx」をご利用いただき誠にありがとうございます。</p>
<p>パスワード再設定用のURLをお送りします。</p>
<br/>
<a href="{{ $url }}">{{ $url }}</a><br>
<br/>
<p>24時間以内に上記のURLにアクセスし、パスワード再設定の手続きをお願いいたします。</p>
<br/>
<p>このメールは「xxxx」より自動送信されております。</p>
<p>ご返信できませんのでご了承ください。</p>
<br/>
<br/>
<br/>
<p>
----------------------------
署名など
----------------------------
</p>
10. コントローラにメール送信処理を追加
コントローラに、①〜④のコード(参照:/* ここから追加 */
)を記述します。
Tips:
-
Log::info
メール送信に成功した場合/Log::errror
メール送信に失敗した場合
storage/logs/laravel.logにログが出力されます。特にエラーになった際、原因を特定しやすくなります。 -
Mail::send(new ResetPasswordMail($user, $userToken));
ResetPasswordMailクラスを呼び出し、メール送信の処理を行います。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
/* ①ここから追加 */
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Http\Requests\ResetInputMailRequest;
use App\Mail\ResetPasswordMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Exception;
/* ①ここまで追加 */
class UsersController extends Controller
{
/* ②ここから追加 */
private $userRepository;
private const MAIL_SENDED_SESSION_KEY = 'user_reset_password_mail_sended_action';
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
/* ②ここまで追加 */
/**
* パスワードリセット
*/
// パスワード再設定用のメール送信フォーム
public function requestResetPassword()
{
return view('users.reset_input_mail');
}
// メール送信
public function sendResetPasswordMail(ResetInputMailRequest $request)
{
/* ③ここから追加 */
try {
// ユーザー情報取得
$user = $this->userRepository->findFromMail($request->mail);
$userToken = $this->userRepository->updateOrCreateUser($user->id);
// メール送信
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信します。');
Mail::send(new ResetPasswordMail($user, $userToken));
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信しました。');
} catch(Exception $e) {
Log::error(__METHOD__ . '...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = ' . $request->mail . ' error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', '処理に失敗しました。時間をおいて再度お試しください。');
}
// 不正アクセス防止セッションキー
session()->put(self::MAIL_SENDED_SESSION_KEY, 'user_reset_password_send_email');
/* ③ここまで追加 */
return redirect()->route('reset.send.complete');
}
// メール送信完了
public function sendCompleteResetPasswordMail()
{
/* ④ここから追加 */
// 不正アクセス防止セッションキーを持っていない場合
if (session()->pull(self::MAIL_SENDED_SESSION_KEY) !== 'user_reset_password_send_email') {
return redirect()->route('reset.form')
->with('flash_message', '不正なリクエストです。');
}
/* ④ここまで追加 */
return view('users.reset_input_mail_complete');
}
// 省略
「②メール送信の実装」は以上になります。
ここまでの実装で、フォームに入力したメールアドレスに、パスワード再設定用のURLを記載したメールを送信する 実装ができました。
ここからは、パスワード再設定用のURLにアクセスし、DBのパスワードを更新する に箇所あたる、「③パスワード更新の実装」を行います。
11. バリデーションの実装_part2
以下2つのバリデーションを実装します。
- パスワード再設定フォーム用
- トークンの有効期限確認用(オリジナルバリデーション)
パスワード再設定フォーム用バリデーションの実装
以下コマンドを実行し、リクエストファイルを作成します。
php artisan make:request ResetPasswordRequest
上記で作成したファイルに、以下コードを記述します。
Tips:
-
regex:/^[0-9a-zA-z-_]{8,32}$/
半角英数字, -, _ のみ入力を許可し、8文字〜32文字で入力されているか判定します。
こちらの正規表現はあくまで一例ですので、アレンジして使ってください。 -
confirmed
再入力欄と一致しているか判定します。 -
new \App\Rules\TokenExpirationTimeCheck()
トークンの有効期限をチェックするオリジナルバリデーションを呼び出しています。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
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<string, mixed>
*/
public function rules()
{
$rules = [
'password' => ['required', `regex:/^[0-9a-zA-z-_]{8,32}$/`, 'confirmed'],
'password_confirmation' => ['required', 'same:password'],
'reset_token' => ['required', new \App\Rules\TokenExpirationTimeCheck()],
];
return $rules;
}
/**
* エラーメッセージ
* @return array
*/
public function messages()
{
return [
'password.required' => 'パスワードを入力してください',
'password.regex' => 'パスワードは半角英数字とハイフンとアンダーバーのみで8文字以上32文字以内で入力してください',
'password.confirmed' => 'パスワードが再入力欄と一致していません',
];
}
}
トークンの有効期限確認用バリデーションの実装
以下コマンドを実行し、オリジナルバリデーションファイルを作成します。
php artisan make:rule TokenExpirationTimeRule
上記で作成したファイルに、以下コードを記述します。
Tips:
- URLの有効期限が切れている場合
メール記載のリンクにアクセスすることは可能ですが、パスワードを更新しようとした際に、こちらのバリデーションに引っかかりエラーメッセージが出力されます。
<?php
namespace App\Rules;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Illuminate\Contracts\Validation\Rule;
use Carbon\Carbon;
class TokenExpirationTimeCheck 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)
{
$now = Carbon::now();
$userRepository = app()->make(UserRepositoryInterface::class);
$userToken = $userRepository->getUserTokenFromUser($value);
$expireTime = new Carbon($userToken->rest_password_expire_data);
return $now->lte($expireTime);
}
/**
* エラーメッセージ
*
* @return string
*/
public function message()
{
return '有効期限が過ぎています。パスワード再設定用のメールを再発行してください。';
}
}
12. リポジトリパターンの実装_part2
インタフェースとリポジトリに、パスワード更新の処理を実装します。
インタフェースに追加実装
パスワードリセット用トークンを発行 updateOrCreateUser()
の下に以下2つのファンクションを追加します。
// 省略
/**
* トークンからユーザー情報を取得
* @param string $token
* @return User
*/
public function getUserTokenFromUser(string $token): User;
/**
* パスワード更新
*
* @param string $password
* @param int $id
* @return void
*/
public function updateUserPassword(string $password, int $id): void;
}
リポジトリに追加実装
インタフェースに追加した2つのファンクションについて、具体的な処理(参照:/* ここから追加 */
)を追加します。
// 省略
// userテーブルに登録
public function updateOrCreateUser(int $userId): User
{
$now = Carbon::now();
// $userIdをハッシュ化
$hashedToken = hash('sha256', $userId);
return $this->userToken->updateOrCreate(
[
'id' => $userId,
],
[
// $hashedTokenを含むトークンを作成
'rest_password_access_key' => uniqid(rand(), $hashedToken),
// トークンの有効期限を現在から24時間後に設定
'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
]);
}
/* ここから追加 */
// トークンからユーザー情報を取得
public function getUserTokenFromUser(string $token): User
{
return $this->userToken->where('rest_password_access_key', $token)->firstOrFail();
}
// パスワード更新
public function updateUserPassword(string $password, int $id): void
{
$this->user->where('id', $id)->update(['password' => $password]);
}
/* ここまで追加 */
}
13. コントローラにパスワード更新処理を追加
コントローラに、①②のコード(参照:/* ここから追加 */
)を記述します。
// 省略
/* ①ここから追加 */
use App\Http\Requests\ResetPasswordRequest;
/* ①ここまで追加 */
class UsersController extends Controller
{
// 中略
/* ②ここから追加 */
// パスワード再設定
public function resetPassword(Request $request)
{
// 署名付きURLではない場合
if (!$request->hasValidSignature()) {
abort(403, 'URLの有効期限が過ぎたためエラーが発生しました。パスワード再設定メールを再発行してください。');
}
$resetToken = $request->reset_token;
try {
// ユーザー情報取得
$userToken = $this->userRepository->getUserTokenFromUser($resetToken);
} catch (Exception $e) {
Log::error(__METHOD__ . ' UserTokenの取得に失敗しました。 error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', __('パスワード再設定メールに添付されたURLから遷移してください。'));
}
return view('users.reset_input_password', compact('userToken', 'userMail'));
}
// パスワード更新
public function updatePassword(ResetPasswordRequest $request)
{
try {
// ユーザー情報取得
$userToken = $this->userRepository->getUserTokenFromUser($request->reset_token);
// パスワード暗号化
$password = encrypt($request->password);
$this->userRepository->updateUserPassword($password, $userToken->id);
Log::info(__METHOD__ . '...ID:' . $userToken->user_id . 'のユーザーのパスワードを更新しました。');
} catch (Exception $e) {
Log::error(__METHOD__ . '...ユーザーのパスワードの更新に失敗しました。...error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', __('処理に失敗しました。時間をおいて再度お試しください。'));
}
return view('users.reset_input_password_complete');
}
/* ②ここまで追加 */
}
以上でパスワードリセット機能の実装が全て完了しました!
次章にて、ファサードでの実装をご紹介しています。
14. おまけ:ファサードで実装
処理の手順に関しては、リポジトリパターンと同様です。
変更点
リポジトリパターンで使用した、以下コードは使用しません。
- プロバイダー
- インタフェース
- リポジトリ
ファサードで実装にあたり、以下ファイルの新規作成・修正を行います。
- app/Fasades/UsersService(新規)
- app/Services/UsersService(新規)
- UsersController(修正)
ファサードクラスの作成
Facadeを継承したUsersServiceクラスを作成します。
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class UsersService extends Facade
{
protected static function getFacadeAccessor()
{
return \App\Services\UsersService::class;
}
}
サービスクラスの作成
上記で作成したファサードを使用して以下サービスクラスを呼び出し、その中に実際の処理を記述します。
サービスクラスに記述にした処理は、コントローラで呼び出して使います。
<?php
namespace App\Services;
use Illuminate\Http\Request;
use Carbon\Carbon;
class UsersService
{
/**
* パスワードリセット
*/
// メールアドレスからユーザー情報取得
public function findFromMail(string $mail)
{
$user = \App\Models\User::where('mail', $mail)->first();
return $user;
}
// トークン, 有効期限を登録
public function updateOrCreateUser(int $userId)
{
$now = Carbon::now();
// $userIdをハッシュ化
$hashedToken = hash('sha256', $userId);
$userToken = \App\Models\User::updateOrCreate(
[
'id' => $userId,
],
[
// $hashedTokenを含むトークンを作成
'rest_password_access_key' => uniqid(rand(), $hashedToken),
// トークンの有効期限を現在から24時間後に設定
'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
]);
return $userToken;
}
// トークンからユーザー情報を取得
public function getUserTokenFromUser(string $token)
{
$userToken = \App\Models\User::where('rest_password_access_key', $token)->first();
return $userToken;
}
// トークンからメールアドレスを取得
public function getUserMailByResetToken(string $resetToken)
{
$userMail = \App\Models\User::select('mail')
->where('rest_password_access_key', $resetToken)->first();
return $userMail;
}
// パスワード更新
public function updateUserPassword(string $password, int $userId)
{
\App\Models\User::where('id', $userId)->update(['password' => $password]);
}
}
コントローラの修正
UsersControllerを以下の通り修正します。
// 省略
/**
* パスワードリセット
*/
public function requestResetPassword()
{
return view('users.reset_input_mail');
}
// パスワード再設定用のメール送信
public function sendResetPasswordMail(ResetInputMailRequest $request)
{
try {
// ユーザー情報取得
$user = UsersService::findFromMail($request->mail);
$userToken = UsersService::updateOrCreateUser($user->id);
// メール送信
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信します。');
Mail::send(new ResetPasswordMail($user, $userToken));
Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信しました。');
} catch(Exception $e) {
Log::error(__METHOD__ . '...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = ' . $request->mail . ' error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', '処理に失敗しました。時間をおいて再度お試しください。');
}
// 不正アクセス防止セッションキー
session()->put(self::MAIL_SENDED_SESSION_KEY, 'user_reset_password_send_email');
return redirect()->route('reset.send.complete');
}
// メール送信完了
public function sendCompleteResetPasswordMail()
{
// 不正アクセス防止セッションキーを持っていない場合
if (session()->pull(self::MAIL_SENDED_SESSION_KEY) !== 'user_reset_password_send_email') {
return redirect()->route('reset.form')
->with('flash_message', '不正なリクエストです。');
}
return view('users.reset_input_mail_complete');
}
// パスワード再設定
public function resetPassword(Request $request)
{
// 署名付きURLではない場合
if (!$request->hasValidSignature()) {
abort(403, 'URLの有効期限が過ぎたためエラーが発生しました。パスワード再設定メールを再発行してください。');
}
$resetToken = $request->reset_token;
$userMail = UsersService::getUserMailByResetToken($resetToken);
try {
// ユーザー情報取得
$userToken = UsersService::getUserTokenFromUser($resetToken);
} catch (Exception $e) {
Log::error(__METHOD__ . ' UserTokenの取得に失敗しました。 error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', __('パスワード再設定メールに添付されたURLから遷移してください。'));
}
return view('users.reset_input_password', compact('userToken', 'userMail'));
}
// パスワード更新
public function updatePassword(ResetPasswordRequest $request)
{
try {
// ユーザー情報取得
$userToken = UsersService::getUserTokenFromUser($request->reset_token);
// パスワード暗号化
$password = encrypt($request->password);
UsersService::updateUserPassword($password, $userToken->id);
Log::info(__METHOD__ . '...ID:' . $userToken->user_id . 'のユーザーのパスワードを更新しました。');
} catch (Exception $e) {
Log::error(__METHOD__ . '...ユーザーのパスワードの更新に失敗しました。...error_message = ' . $e);
return redirect()->route('reset.form')
->with('flash_message', __('処理に失敗しました。時間をおいて再度お試しください。'));
}
return view('users.reset_input_password_complete');
}
// 省略
15. 参考
実装内容は以下サイトを参考にさせていただきました。