Laravel

Laravel パスワードリセット機能の実装方法を解説【メールを送信し署名付きURLからリセットする】

こんにちは。Laravelエンジニアのタクマです。

今回は、Laravelのパスワードリセット機能の実装方法を解説します。

・パスワードリセット機能の実装方法がわからない

・有効期限つきメールの作成方法がわからない

こんなお悩みを解決できる記事となっております。

実装するパスワードリセット機能は以下のような流れのものになります。

  1. パスワードリセットを希望するユーザーにパスワードリセットメールを送信
  2. パスワードリセットメールに添付する署名付きURLには有効期限を設定
  3. 有効期限内にパスワードリセットメールに添付されている署名付きURLにアクセスをし、パスワードを更新する

上記の流れを実装しながら詳しく解説していきます。

前提条件

  1. passwordカラムを含むusersテーブルは作成されているものとする
  2. userのログイン機能は実装できているものとする
  3. バージョン: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には外部キー制約を設定しています。

外部キー制約について詳しく知りたい方は、僕が以前書いたこちらの記事をご覧ください。

Laravel 外部キー制約の設定方法【2つの方法を解説】laravelで外部キー制約を設定する方法を解説します。 foreign() references() on()を使った書き方、foreignId() constrained()を使った書き方の2つの方法について解説しています。...

トークンは重複しないよう、ユニーク制約を設定しています。

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について詳しく解説しています。

Laravel 最強のemail バリデーションを伝授します【email:filter / email:dnsも解説】Laravelのemailバリデーションであるemail:filter email:dnsを詳しく解説します。...

exists:users,emailは、入力されたメールアドレスがusersテーブルに登録されているメールアドレスかを確認します。

入力されたメールアドレスが、usersテーブルに登録されていない場合、バリデーションで弾きます。

次に、PasswordControllersendEmailResetPasswordアクションを追加します。

<?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アクションでリポジトリパターン(InterfaceRepository)が登場しました。

今回は本題ではないためリポジトリパターンの詳しい説明は割愛しますが、登録の手順は説明しますのでご安心ください。

リポジトリパターンの実装

まずは、InterfaceRepositorybindする(紐づける)ためのプロバイダを作成します。

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()
    {
        //
    }
}

上記のように記述することで、今後InterfaceRepositoryが増えても、$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がプロバイダとして登録され、リポジトリパターンが機能するようになりました。

では、InterfaceRipositoryを作成します。

まずは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は第一引数に与えられた$userIduser_tokenテーブルに存在しない場合は新規で作成され、存在する場合はそのレコードを更新してくれるとても便利なメソッドです。(参考:Laravel 8.x Eloquentの準備

  • 'expire_at' => $now->addHours(48)->toDateTimeString()でトークンの有効期限を現在から48時間後としています。

パスワードリセットメールの作成

次にメールを作成します。

PasswordControllersendEmailResetPasswordアクションの下記の部分です。

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.phpsendCompleteアクションを追加します。

   /**
    * ユーザーのパスワードリセットメール送信完了ページ
    *
    * @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.phpeditアクションを追加します。

    /**
    * ユーザーのパスワード再設定フォーム画面
    *
    * @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);
    }

userTokenRepositorygetUserTokenfromToken関数を実装します。

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.phpupdateアクションを追加します。

下記を追加してください。

<?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でユーザーのパスワードの更新処理を行っています。

以上でパスワード更新処理の実装が終わりました。

いよいよ次が最後のパートです。

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

パスワード再設定完了ページを作成します。

まずは、PasswordControllereditedアクションを追加します。

    /**
    * パスワードリセット完了ページ
    *
    * @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でも情報発信をしていますので、よかったらフォローをお願いします(^^)

Webエンジニア
タクマ
埼玉県出身の33歳

新卒で入社した専門商社で8年間営業職として勤務

30歳からプログラミングを始め31歳でWebエンジニアに転職成功

受託開発企業での開発を1年弱経験したのち、現在はスタートアップの自社開発企業で開発に従事している
\ Follow me /

POSTED COMMENT

  1. タカタカ より:

    お疲れ様です。
    laravel学習中の初学者なのですが、リポジトリパターン作成の時の
    “`
    Eloquentsディレクトリ内にUserRepository.phpを作成し、下記のように編集します。
    “`
    は同じ名前のものが二つになるので、もしかしたら「UserTokenRepository」ではないでしょうか?

    それからコピペして気づいたのですが、UserTokenRepositoryの内容の最後に「}」が足りない気がします。

    あと質問なのですが、「パスワードリセットメールの作成」画面の「’送信元のメールアドレス’, ‘送信元の名前’」部分は、どの様に記載すればメールアドレスと名前を指定できますか?初歩的な質問ですみません。

  2. タクマ より:

    タカタカさん

    返信遅くなってしまい大変申し訳ございません。

    > もしかしたら「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/ の記事で解説しているのでご参考になれば幸いです。

  3. l love lalavel より:

    初めてlaravelアプリを作る中で、タクマさんがお作りになったものを参考に、自分のアプリに導入させていただきました。
    XAMPを使って開発環境では、メールも送れてパスワードの変更もできておりました。
    これを、VPS、CentOSの公開環境にデプロイして、パスワードの再設定を試したところ、何度やってもメールの送信に失敗します。メールはユーザー登録時に送るようにしていますが、こちらは問題なく送れます。
    色々探ってはみましたが、どうしてもわからず、ここはタクマさんにお知恵を拝借するしかないと思い、ご連絡した次第です。つまらない質問かもとは思いますが、ご教授のほどよろしくお願いします。

  4. タクマ より:

    I love laravelさん

    返信遅くなってしまい申し訳ありません。

    すみません、I love laravelさんの環境を再現できるわけではないので原因は私も分かりかねますm(_ _)m

    エラーログは出ていませんでしょうか?

    • l love lalavel より:

      ご回答ありがとうございます。
      エラーログは、以下となります。
      これを読むと、接続できないようですが、どこに誤りがあるのかわかりません。初めに申しましたようにユーザー登録時に送られるメールは遅れています。その場合は、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

  5. タクマ より:

    I love laravelさん

    返信遅くなってしまい申し訳ありません。

    メールドライバの設定はしていますでしょうか?
    していないようでしたら https://readouble.com/laravel/8.x/ja/mail.html を参考にお使いのドライバの設定をしてみてください。

    もしくは、このパスワードリセットメールもユーザー登録時のメール送信と同じようにJobを使ってみてはいかがでしょうか?

  6. l love lalavel より:

    いろいろ調べていったところ、SELinuxに原因がありました。あまり理解しておらず、Enfocingモードになっていましたので、SELinuxからけられていました。Permissiveモードへ変更し、問題個所を設定しないしたところ、再度Enforcingモードに戻してもOKでした。
    タクマさんがつくられたすばらい機能を活用させていただくことができるようになってうれしいです。
    セキュリティ面で感動しました。
    リセットをリクエストしたPCとブラウザが同じでないと、設定画面が表示されないのはgood job!です。
    今回のトラブルで、私の知識もぐんと飛躍することができました。本当にありがとうございました。
    さいごに、お気づきとは思いますが、ニックネームのlalavelはrとlの間違いでして、笑ってやってください。

  7. タクマ より:

    I love laravelさん

    解決されたようでよかったです!

    そう言っていただけると私も嬉しいです!ありがとうございます!

    > さいごに、お気づきとは思いますが、ニックネームのlalavelはrとlの間違いでして、笑ってやってください。

    全然気づいていませんでした。笑

タクマ へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA