PHP
laravel
laravel5
laravel5.6
メール認証

[Laravel]メール認証を使った会員登録

今回は、Laravel5(5.6)にてメール認証を行った会員登録の実装を紹介します。

実務でLaravelのサイトを作ったときメール認証の実装方法が日本語で解説されていないと感じたので。
手順は良いからソース知りたいって人→ココ

実装機能

  1. 仮会員登録
  2. メール認証
  3. 本会員登録

流れ

  1. 仮会員としてメールアドレスを登録
  2. 本会員登録用URLを添付したメール送信
  3. URLクリックで入力フォームへ(メール認証)
  4. 本会員情報の入力
  5. 本会員登録完了

画面の流れ

今回は、以下の画面構成で考えました。

  1. 仮会員登録画面
  2. 確認画面
  3. 完了画面
  4. (仮会員登録認証メール)
  5. 本会員登録画面
  6. 確認画面
  7. 完了画面

実装

さて、実装に入りましょう。

仮会員登録

まだ、make:authを実行していない方は先に実行してください。

php artisan make:auth

実行済みの方は、次に進みます。

Usersテーブルにtokenを追加する

仮会員登録で発行するURLtokenのフィールドを、Usersテーブルに追加します。

Artisanコマンドでmigrationファイルを作成しましょう。

php artisan make:migration add_columns_users_table

補足:--tableオプションでテーブル名を指定してもいいです。

php artisan make:migration add_votes_to_users_table --table=users

作成したmigrationファイルに2カラム追加します。
1. email_verified : 認証済みかどうか
2. email_token : email用トークン

スキーマの内容は以下のようにします。

class AddColumnsUsersTable extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->tinyInteger('email_verified')->default(0);
            $table->string('email_verify_token')->nullable();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('email_verified');
            $table->dropColumn('email_verify_token');
        });
    }
}

重要:down()に記述を忘れずに入力してください。migrate:rollbackコマンドを実行したとき、down()の処理を行います。up()に対する逆の処理がdown()に正しく記載されていない場合、migrate:rollbackmigrate:resetが失敗してしまいます。

migrationファイルの作成が終わったら、忘れずにmigrateコマンドでmigrationの適用をしてください。

php artisan migrate

.envファイルの変更

.envファイルにemail設定を追加します。
ここでは、mailtrapを使用して実装しています。
アカウントを作るだけなので簡単です。詳しくはこちらMailtrapでLaravelの簡単メール送信テストなどを参考にしてください。

.envファイルは以下のように修正します。

 .env
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=fec77971a86b66
MAIL_PASSWORD=9c480963d8d7df
MAIL_ENCRYPTION = tls
+ MAIL_FROM_ADDRESS=from@example.com
+ MAIL_FROM_NAME=LaravelExampler
  1. MAIL_FROM_ADDRESS
  2. MAIL_FROM_NAME

上記を追加することで、メール送信処理のアドレスと送信者名のデフォルト値を設定することができます。

メール作成

認証メールに使用するviewと、tokenを返すクラスを作成します。

php artisan make:mail EmailVerification

このコマンドで、app/MailにMailableを継承したEmailVerificationクラスが作成されます。
このクラスにメール送信に使用するviewやメールタイトルなどの設定を記述することになります。

EmailVerificationの設定

このクラスには2つのメソッドがあります。

  1. constructor()
  2. build()

コンストラクタはご存知の通りです。buildは実行時に呼ばれる関数です。

コンストラクタに、Users情報を渡しましょう。

クラス変数を追加します。

protected $user;

constructor()で引数に$userを設定し、クラス変数にセットします。

public function __construct($user)
    {
        $this->user = $user;
    }

補足:こうすることで、build()にて$this->user->email_verify_tokenと参照することができます。(ユーザーごとのtokenを判別できます)

build()を更新します。

    public function build()
    {
        return $this
            ->subject('【site】仮登録が完了しました')
            ->view('auth.email.pre_register')
            ->with(['token' => $this->user->email_verify_token,]);
    }
}

これで、viewと一緒にtokenを渡すことができました!

Emailテンプレートの作成

さて、今度はEmailの本文となるviewを作りましょう。

今回はviews/auth/emailディレクトリにpre_register.blade.phpを作成しました。(イケてるファイル名に差し替えて使ってください。)

サイトへのアカウント仮登録が完了しました<br>
<br>
以下のURLからログインして本登録を完了させてください<br>
{{url('register/verify/'.$token)}}

Userモデルの更新

Userに追加したカラムにデータをセットするには、一手間いります。
Laravelでは、モデルへのカラムへの挿入時には$fillableもしくは$guardedにカラムを設定する必要があります。

詳しくはこちらなどを参照ください:
【Laravel:Eloquentクラス】fillableとguardedの指定はどちらかだけでいい

それではUser.phpへ追加したカラムを$fillableに設定しましょう。

    protected $fillable = [
        'name', 'email', 'password',
+        'email_verified', 'email_verify_token',
    ];

仮会員登録フォームの追加

仮会員登録時に「メールアドレス」「パスワード」の登録を行う仕様にします。
make:authで作成したものは「名前」が入っているので取り除きましょう。

register.blade.phpから名前の項目を削除します。

-                        <div class="form-group row">
-                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
-
-                            <div class="col-md-6">
-                                <input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>
-
-                                @if ($errors->has('name'))
-                                    <span class="invalid-feedback">
-                                        <strong>{{ $errors->first('name') }}</strong>
-                                    </span>
-                                @endif
-                            </div>
-                        </div>

register.blade.phpのformの送信先を変更します。

-                    <form method="POST" action="{{ route('register') }}">
+                    <form method="POST" action="{{ route('register.pre_check') }}">

image

仮会員確認

次に、仮登録確認画面のviewを追加しましょう。

views/authregister_check.blade.phpを追加します。

register_check.blade.php
@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">
                    <form method="POST" action="{{ route('register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">メールアドレス</label>

                            <div class="col-md-6">
                                <span class="">{{$email}}</span>
                                <input type="hidden" name="email" value="{{$email}}">
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">パスワード</label>

                            <div class="col-md-6">
                                <span class="">{{$password_mask}}</span>
                                <input type="hidden" name="password" value="{{$password}}">
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    仮登録
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

web.php(route)に追加します。

Route::post('register/pre_check', 'Auth\RegisterController@pre_check')->name('register.pre_check');

RegisterControllerの更新

「名前」を削除したので、Controllerに反映しましょう。
Validator()から'name'の行を削除してください。

    protected function validator(array $data)
    {
        return Validator::make($data, [
-            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

削除しないままだと、存在しない項目のバリデーションが実行されることになります。

次に、確認画面のビューに遷移できるように修正しましょう。
web.phpに記載したメソッド:pre_check()を追加し、returnにviewを返します。
ここでは、受け取ったリクエストのバリデーションチェックを行い、ビューに値を渡します。
passwordは、表示するときにマスキングするようにしています。

RegisterController.php
    public function pre_check(Request $request){
        $this->validator($request->all())->validate();
        //flash data
        $request->flashOnly( 'email');

        $bridge_request = $request->all();
        // password マスキング
        $bridge_request['password_mask'] = '******';

        return view('auth.register_check')->with($bridge_request);
    }

これで、仮会員登録画面〜確認画面までが実装できました。

image.png

仮登録完了画面の追加

views/authregistered.blade.phpを追加してください。

registered.blade.php
@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">
                    <p>この度は、ご登録いただき、誠にありがとうございます。</p>
                    <p>
                        ご本人様確認のため、ご登録いただいたメールアドレスに、<br>
                        本登録のご案内のメールが届きます。
                    </p>
                    <p>
                        そちらに記載されているURLにアクセスし、<br>
                        アカウントの本登録を完了させてください。
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

RegisterControllerのcreate()email_verify_tokenを追加します。
ここで、作成したEmailVerificationを使用し、Mail::を使用してメール送信処理を実行します。

    protected function create(array $data)
    {
-        return User::create([
-            'email' => $data['email'],
-            'password' => Hash::make($data['password']),
-        ]);

+        $user = User::create([
+            'email' => $data['email'],
+            'password' => Hash::make($data['password']),
+            'email_verify_token' => base64_encode($data['email']),
+        ]);

+        $email = new EmailVerification($user);
+        Mail::to($user->email)->send($email);

+        return $user;
    }

/registerで呼ばれるメソッドregister()に上記のcreate()メソッドを紐づけます。

    public function register(Request $request)
    {
        event(new Registered($user = $this->create( $request->all() )));

        return view('auth.registered');
    }

補足:/registerは、make:authコマンドでweb.phpに作成される、Auth::routes()に含まれています。
確認したい場合は、php artisan route:listコマンドを実行してみてください。

今のままだと、UsersテーブルのnameがNot nullになっているのでエラーになってしまいます。
namenullableを設定しましょう。

php artisan make:migration change_column_users_table --table=users

image.png

ここまでで、仮会員登録が実装できました。
うまくできないときは、こちらのソースと比較して見てください

本会員登録フォームの追加

本会員登録用URLがクリックされると本会員登録フォームに遷移されるようにしましょう。
ルーティング(web.php)に以下を追加します。

Route::get('register/verify/{token}', 'Auth\RegisterController@showForm');

RegisterControllerにshowForm()を追加しましょう。
エラーチェックをした後、Viewを返す処理をしています。

    public function showForm($email_token)
    {
        // 使用可能なトークンか
        if ( !User::where('email_verify_token',$email_token)->exists() )
        {
            return view('auth.main.register')->with('message', '無効なトークンです。');
        } else {
            $user = User::where('email_verify_token', $email_token)->first();
            // 本登録済みユーザーか
            if ($user->status == config('const.USER_STATUS.REGISTER')) //REGISTER=1
            {
                logger("status". $user->status );
                return view('auth.main.register')->with('message', 'すでに本登録されています。ログインして利用してください。');
            }
            // ユーザーステータス更新
            $user->status = config('const.USER_STATUS.MAIL_AUTHED');
            $user->verify_at = Carbon::now();
            if($user->save()) {
                return view('auth.main.register', compact('email_token'));
            } else{
                return view('auth.main.register')->with('message', 'メール認証に失敗しました。再度、メールからリンクをクリックしてください。');
            }
        }
    }

以下のときをエラーをとしています。

  1. 登録済みのトークンのとき
  2. メール認証済みのユーザーのとき
  3. 本登録済みのユーザーのとき

エラーのときは、Viewにメッセージを通知するようにしています。

statusカラム追加

メール認証済みかどうかは、email_verifyカラムで扱うようにしていますが、「本登録済み」かなどのステータスも扱いたいと思います。
statusカラムをUserテーブルに追加しましょう。
email_verifyと内容が被るかもしれませんが、ここでは別々に扱うようにしています。)

migrationファイルを作成します。

$ php artisan make:migration add_column_users_table --table=users
class AddColumnUsersTable extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->tinyInteger('status')->default(0);
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('status');
        });
    }
}

migrateしましょう。

config: constの追加

showForm()config('const.USER_STATUS.REGISTER')の記述がありますが、これはステータス値をconst(定数)で管理するようにしています。
config/const.phpを作成し、以下のように設定してください。

config/const.php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Const
    |--------------------------------------------------------------------------
    */

    // 0:仮登録 1:本登録 2:メール認証済 9:退会済
    'USER_STATUS' => ['PRE_REGISTER' => '0', 'REGISTER' => '1', 'MAIL_AUTHED' => '2', 'DEACTIVE' => '9',],
];

こうすることで、config('const.XXX')で設定値を参照することができます。
配列のときはconfig('const.array.XXX')のように書きます。

viewの追加

次に、Viewを追加しましょう。
showFomr()return view('auth.main.register')と書きましたね。このviewを作成します。

auth/main/register.blade.php

@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>

                    @isset($message)
                        <div class="card-body">
                            {{$message}}
                        </div>
                    @endisset

                    @empty($message)
                        <div class="card-body">
                            <form method="POST" action="{{ route('register.pre_check') }}">
                                @csrf
                                <div class="form-group row">
                                    <label for="name" class="col-md-4 col-form-label text-md-right">名前</label>
                                    <div class="col-md-6">
                                        <input
                                            id="name" type="text"
                                            class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}"
                                            name="name" value="{{ old('name') }}" required>

                                        @if ($errors->has('name'))
                                            <span class="invalid-feedback">
                                            <strong>{{ $errors->first('name') }}</strong>
                                            </span>
                                        @endif
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="name_pronunciation"
                                           class="col-md-4 col-form-label text-md-right">フリガナ</label>

                                    <div class="col-md-6">
                                        <input id="name_pronunciation" type="text"
                                               class="form-control{{ $errors->has('name_pronunciation') ? ' is-invalid' : '' }}"
                                               name="name_pronunciation" value="{{ old('name_pronunciation') }}"
                                               required>

                                        @if ($errors->has('name_pronunciation'))
                                            <span class="invalid-feedback">
                                            <strong>{{ $errors->first('name_pronunciation') }}</strong>
                                            </span>
                                        @endif
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="name_pronunciation"
                                           class="col-md-4 col-form-label text-md-right">生年月日</label>
                                    <div class="col-md-6">
                                        <div class="row">
                                            <div class="col-md-4">
                                                <select id="birth_year" class="form-control" name="birth_year">
                                                    <option value="">----</option>
                                                    @for ($i = 1980; $i <= 2005; $i++)
                                                        <option value="{{ $i }}"
                                                                @if(old('birth_year') == $i) selected @endif>{{ $i }}</option>
                                                    @endfor
                                                </select>
                                                @if ($errors->has('birth_year'))
                                                    <span class="help-block">
                                                        <strong>{{ $errors->first('birth_year') }}</strong>
                                                    </span>
                                                @endif
                                            </div>年

                                            <div class="col-md-3">
                                                <select id="birth_month" class="form-control" name="birth_month">
                                                    <option value="">--</option>
                                                    @for ($i = 1; $i <= 12; $i++)
                                                        <option value="{{ $i }}"
                                                            @if(old('birth_month') == $i) selected @endif>{{ $i }}</option>
                                                    @endfor
                                                </select>
                                                @if ($errors->has('birth_month'))
                                                    <span class="help-block">
                                                        <strong>{{ $errors->first('birth_month') }}</strong>
                                                    </span>
                                                @endif
                                            </div>月

                                            <div class="col-md-3">
                                                <select id="birth_day" class="form-control" name="birth_day">
                                                    <option value="">--</option>
                                                    @for ($i = 1; $i <= 31; $i++)
                                                        <option value="{{ $i }}"
                                                            @if(old('birth_day') == $i) selected @endif>{{ $i }}</option>
                                                    @endfor
                                                </select>

                                                @if ($errors->has('birth_day'))
                                                    <span class="help-block">
                                                        <strong>{{ $errors->first('birth_day') }}</strong>
                                                    </span>
                                                @endif
                                            </div>日
                                        </div>

                                        <div class="row col-md-6 col-md-offset-4">
                                            @if ($errors->has('birth'))
                                                <span class="help-block">
                                                    <strong>{{ $errors->first('birth') }}</strong>
                                                </span>
                                            @endif
                                        </div>
                                    </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    確認画面へ
                                </button>
                            </div>
                        </div>
                        </form>
                </div>
                @endempty
            </div>
        </div>
    </div>
    </div>
@endsection

Viewでは、

  • Controllerからエラーメッセージ($message)があるとき
    • エラーメッセージのみを表示する
  • Controllerからエラーメッセージがないとき
    • 本会員の入力フォームを表示する

としています。

本会員カラムの追加

さて、viewで入力してもらうカラムを追加しましょう。

php artisan make:migration add_columns_users_table --table=user

今回はUsersモデルに追加していきます。

  • 名前
  • フリガナ
  • 生年月日

「名前」はデフォルトでカラムがありますので、それ以外を追加してみましょう。

migration
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
          $table->string('name_pronunciation')->nullable();
          $table->integer('birth_year')->nullable();
          $table->integer('birth_month')->nullable();
          $table->integer('birth_day')->nullable();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
          $table->dropColumn('name_pronunciation');
          $table->dropColumn('birth_year');
          $table->dropColumn('birth_month');
          $table->dropColumn('birth_day');
        });
    }

php artisan migrateを実行します。

確認画面の追加

ControllerにmainCheck()を追加

確認画面のviewを返すメソッドを用意します。
RegisterControllerに追加します。

RegisterController.php
  public function mainCheck(Request $request)
  {
    $request->validate([
      'name' => 'required|string',
      'name_pronunciation' => 'required|string',
      'birth_year' => 'required|numeric',
      'birth_month' => 'required|numeric',
      'birth_day' => 'required|numeric',
    ]);
    //データ保持用
    $email_token = $request->email_token;

    $user = new User();
    $user->name = $request->name;
    $user->name_pronunciation = $request->name_pronunciation;
    $user->birth_year = $request->birth_year;
    $user->birth_month = $request->birth_month;
    $user->birth_day = $request->birth_day;

    return view('auth.main.register_check', compact('user','email_token'));
  }

確認画面では入力内容を表示します。
入力内容でUsersモデルを更新したいので、viewにデータを渡し、次のControllerに渡します。

Viewの作成

register_check.blade.phpを作成します。

register_check.blade.php
@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">
                    <form method="POST" action="{{ route('register.main.registered') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">名前</label>
                            <div class="col-md-6">
                                <span class="">{{$user->name}}</span>
                                <input type="hidden" name="email" value="{{$user->name}}">
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="name_pronunciation" class="col-md-4 col-form-label text-md-right">フリガナ</label>
                            <div class="col-md-6">
                                <span class="">{{$user->name_pronunciation}}</span>
                                <input type="hidden" name="name_pronunciation" value="{{$user->name_pronunciation}}">
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="birth" class="col-md-4 col-form-label text-md-right">生年月日</label>
                            <div class="col-md-6">
                                <span class="">{{$user->birth_year}}年</span>
                                <input type="hidden" name="birth_year" value="{{$user->birth_year}}">
                                <span class="">{{$user->birth_month}}月</span>
                                <input type="hidden" name="birth_month" value="{{$user->birth_month}}">
                                <span class="">{{$user->birth_day}}日</span>
                                <input type="hidden" name="birth_day" value="{{$user->birth_day}}">
                            </div>
                        </div>


                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    本登録
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

ルーティングの追加

ルーティングはこんな感じです。

Route::post('register/main_check', 'Auth\RegisterController@mainCheck')->name('register.main.check');

本登録の実装

ようやく最後の処理です。

確認画面で本登録ボタンを押したら本会員登録されるようにしましょう。

ルーティングの追加

Route::post('register/main_register', 'Auth\RegisterController@mainRegister')->name('register.main.registered');

ControllerにmainRegister()を追加

RegisterController.php
  public function mainRegister(Request $request)
  {
    $user = User::where('email_verify_token',$request->email_token)->first();
    $user->status = config('const.USER_STATUS.REGISTER');
    $user->name = $request->name;
    $user->name_pronunciation = $request->name_pronunciation;
    $user->birth_year = $request->birth_year;
    $user->birth_month = $request->birth_month;
    $user->birth_day = $request->birth_day;
    $user->save();

    return view('auth.main.registered');
  }

viewの追加

auth/main/registered.blade.phpを追加。

auth/main/registered.blade.php
@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">
                        <p>本会員登録が完了しました。</p>
                        <a href="{{url('/')}}" class="sg-btn">トップページへ戻る</a>

                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

完成

お疲れ様です。Laravel5.6でのメール認証は以上です。
あなたのサイトに合わせて内容を変更してみてください!
ソースコードは、こちらから参照ください!

参考

以下のサイトを参考にしています。半分くらいこちらの翻訳記事と言ってもいいかもしれません。

  1. How to Use Queue in Laravel 5.4 For Email Verification 上記サイトではQueueを使用していますが、今回は使用していません。