9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】パスワードリセット機能の実装(有効期限・署名付きURLをメール送信)

Last updated at Posted at 2023-02-26

はじめに

今回は、Laravelでパスワードリセット機能の実装についてまとめました。

※おことわり※
基本的に学習内容のアウトプットです。
初学者であるため、間違い等あればご指摘いただけますと嬉しいです。

この記事の目的

以下内容のアウトプット

  • カラムの追加 ・・・マイグレーション機能
  • メール送信機能の実装 ・・・Mailableクラス
  • パスワードリセット機能を実装 ・・・リポジトリパターン
  • パスワードリセット機能を実装 ・・・ファサード
  • トークンの有効期限の判定 ・・・オリジナルバリデーション

この記事の内容

  1. 前提
  2. 実装内容と大まかな手順
  3. 既存テーブルにカラムの追加
  4. ルーティング・コントローラの設定
  5. ビューの作成
  6. プロバイダの作成・登録
  7. バリデーションの実装_part1
  8. リポジトリパターンの実装_part1
  9. Mailableクラスの実装
  10. コントローラにメール送信処理を追加
  11. バリデーションの実装_part2
  12. リポジトリパターンの実装_part2
  13. コントローラにパスワード更新処理を追加
  14. おまけ:ファサードで実装
  15. 参考

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. ルーティング・コントローラの設定

ルーティングとコントローラの設定を行います。

ルーティングの設定

パスワードリセット関連のルーティングを設定ます。

web.php

// 省略

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 ディレクトリ/コントローラ名 を実行し、コントローラを作成します。
UsersController
<?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>

メール送信フォーム

reset_input_mail.blade.php
@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

メール送信完了ページ

reset_input_mail_complete.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>メール送信完了</h2>
    <div>
        <p>パスワード再設定用のメールを送信しました</p>
        <p>メールに記載されているリンクからパスワードの再設定を行ってください</p>
    </div>
    <div>
      <a href="{{ route('xxxx') }}">ログイン画面へ</a>
    </div>
</main>
@endsection

パスワード再設定フォーム

reset_input_password.blade.php
@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

パスワード再設定完了ページ

reset_input_password_complete.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>パスワード変更完了</h2>
    <div>
        <p>パスワードの変更が完了しました</p>
        <p>新しいパスワードにて再ログインしてください</p>
    </div>
    <div>
      <a href="{{ route('xxxx') }}">ログイン画面へ</a>
    </div>
</main>
@endsection

6. プロバイダの作成・登録

インタフェースとリポジトリを紐づけるためのプロバイダを作成し、設定用ファイルに追加します。

プロバイダの作成

以下コマンドを実行し、プロバイダファイルを作成します。

terminal
php artisan make:provider RepositoryServiceProvider

プロバイダの設定

上記で作成したファイルに、以下コードを記述します。

UserRepositoryInterface, UserRepositoryは後ほど実装します。

Tips:

  • $models 配列 
    今後インタフェースとリポジトリが増えた場合は、配列に追記するだけで bindが行えるように配列を使用しています。
RepositoryServiceProvider.php
<?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.phpproviders配列 に追加します。

Tips:

  • サービスプロバイダクラスとして、提供するサービスが必要な場合にのみ読み込まれます。
config/app.php
'providers' => [
        // 中略
        /* 下記1行追加 */
        App\Providers\RepositoryServiceProvider::class,

    ],

「①パスワードリセット機能実装のための準備」は以上になります。
続いて「②メール送信の実装」を行います。

7. バリデーションの実装_part1

「パスワード再設定用のメールアドレス入力フォーム用」のバリデーションを実装します。

リクエストファイルの作成

以下コマンドを実行し、リクエストファイルを作成します。

terminal
php artisan make:request ResetInputMailRequest

バリデーションの設定

上記で作成したファイルに、バリデーションを定義します。

Tips:

  • email:rfc,dns,filter

    • rfc RFC準拠になっているか判定します。
    • dnc メールアドレスのドメインが有効か判定します。
    • filter 平仮名, カタカナ, 漢字の入力を防ぎ、@の後にドットが最低1つ存在するか判定します。
  • exists:user,mail
    フォームに入力されたメールアドレスが、userテーブルに登録されているか判定します。

ResetInputMailRequest.php
<?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 を作成

作成したファイルに、以下コードを記述します。

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時間後の時間がカラムに登録されます。

UserRepository.php
<?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クラスを継承したファイルを作成します。

terminal
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に以下のように定義します。
    送信元の名前はご自身の環境のものを設定してください。

config/mail.php
from => [
    address => hogefuga@example.com,  // 送信元のメールアドレスと
    name => 山田太郎                   // 送信元の名前
    ],
ResetPasswordMail.php
<?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 を作成

上記で作成したファイルに、メールの本文を記述します。

最低限のコードになりますので、アレンジして使ってください。

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クラスを呼び出し、メール送信の処理を行います。

UsersController
<?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つのバリデーションを実装します。

  • パスワード再設定フォーム用
  • トークンの有効期限確認用(オリジナルバリデーション)

パスワード再設定フォーム用バリデーションの実装

以下コマンドを実行し、リクエストファイルを作成します。

terminal
php artisan make:request ResetPasswordRequest

上記で作成したファイルに、以下コードを記述します。

Tips:

  • regex:/^[0-9a-zA-z-_]{8,32}$/
    半角英数字, -, _ のみ入力を許可し、8文字〜32文字で入力されているか判定します。
    こちらの正規表現はあくまで一例ですので、アレンジして使ってください。

  • confirmed
    再入力欄と一致しているか判定します。

  • new \App\Rules\TokenExpirationTimeCheck()
    トークンの有効期限をチェックするオリジナルバリデーションを呼び出しています。

ResetInputPasswordRequest.php

<?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' => 'パスワードが再入力欄と一致していません',
        ];
    }

}

トークンの有効期限確認用バリデーションの実装

以下コマンドを実行し、オリジナルバリデーションファイルを作成します。

terminal
php artisan make:rule TokenExpirationTimeRule

上記で作成したファイルに、以下コードを記述します。

Tips:

  • URLの有効期限が切れている場合
    メール記載のリンクにアクセスすることは可能ですが、パスワードを更新しようとした際に、こちらのバリデーションに引っかかりエラーメッセージが出力されます。
TokenExpirationTimeCheck.php
<?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つのファンクションを追加します。

UserRepositoryInterface.php
// 省略
      
    /**
     * トークンからユーザー情報を取得
     * @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つのファンクションについて、具体的な処理(参照:/* ここから追加 */)を追加します。

UserRepository.php
// 省略

    // 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. コントローラにパスワード更新処理を追加

コントローラに、①②のコード(参照:/* ここから追加 */ )を記述します。

UsersController.php
// 省略

/* ①ここから追加 */
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クラスを作成します。

app/Fasades/UsersService
<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class UsersService extends Facade
{
    protected static function getFacadeAccessor()
    {
        return \App\Services\UsersService::class;
    }
}

サービスクラスの作成

上記で作成したファサードを使用して以下サービスクラスを呼び出し、その中に実際の処理を記述します。
サービスクラスに記述にした処理は、コントローラで呼び出して使います。

app/Services/UsersService
<?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を以下の通り修正します。

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. 参考

実装内容は以下サイトを参考にさせていただきました。

9
5
1

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
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?