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

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

この記事からわかること

  • Laravelパスワードリセット機能を開発する流れ
  • メール送信方法
  • カスタマイズ日本語化するには?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

公式リファレンス:パスワードリセット(Laravel8系)

パスワードリセット機能

Laravelでは認証機能を比較的簡単に実装することが可能です。それに伴いユーザーがパスワードを忘れてログインできなくなった場合の救済措置として必要な「パスワードリセット機能」の簡単に実装できるようになっています。

今回はLaravel8系においてパスワードリセット機能の実装方法をまとめていきます。また前提として認証機能の実装が完了していることが条件になるので未実装の方は以下の記事を参考にしてください。

実装手順

  1. password_resetsテーブルの作成
  2. パスワードリセットメール送信処理の実装
  3. リセット処理の実装
  4. パスワードリセット入力画面と結果画面のViewを実装
  5. ルーティングの定義
  6. .envにメール設定

1.password_resetsテーブルの作成

パスワードリセット機能を使用するためには対象のメールアドレスと発行用のトークンを管理するpassword_resetsテーブルを作成しておく必要があります。このテーブルのマイグレーションファイルはLaravelがデフォルトで用意してくれているのでphp artisan migrateを実行するだけでOKです。

$ php artisan migrate

2.パスワードリセットメール送信処理の実装

続いてパスワードリセットメール送信処理の実装をしていきます。PasswordResetController(名前は何でも良い)を作成してその中に諸々の処理を記述していきます。

sendResetLinkEmailメソッドを定義しRequest対象のメールアドレスを受け取れるようにしておきます。ここのバリデーション時点でUserEloquentに対象のメールアドレスがそもそも存在するかをチェックしています。

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ではこの実装だけで決まったテンプレートのパスワードリセットメールを送信してくれます。成功/失敗のステータスでレスポンスだけハンドリングしておきます。

実際に以下のようなデザインのメールが届きます。ユーザーはこのボタンをクリックしてパスワードリセット処理を進める流れになります。

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

またこのリンクをタップした際に表示するViewは用意されないので自前で用意する必要があります。これは後述しています。

3.リセット処理の実装

続いて実際にパスワードをリセットする処理を実装していきます。resetメソッドを定義しRequestでは「トークン」、「メールアドレス」、「変更先のパスワード」を受け取ります。

パスワードのリセット処理はPassword::resetで行います。このメソッドでは内部的に以下のことを実行してくれます。

  1. メールアドレスとリセットトークンを検証
  2. usersテーブルのユーザーを取得
  3. $callback を実行して、新パスワードに更新
  4. password_resets テーブルから該当するトークンを削除
  5. 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内のUsersendPasswordResetNotificationメソッドをオーバーライドして先ほど作成した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')
    ]));
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index