Laravel 5.1 のユーザー登録で確認メールを送る【前篇】

Laravel 5.1 では標準でユーザー登録機能がコントローラとして提供されています。しかし、この機能には、確認メールを送る機能が付いていません。今回はユーザー登録に、確認メールの送信機能を追加し、メールアドレスがユーザー登録した本人の物であることを確認できるようにカスタマイズします。


環境設定

データベースとメール送信の環境設定を行います。


ユーザー登録、ログインの実装

# マイグレーション

標準で提供されている、users, password_resets テーブルを作成します。

database/
└── migrations
    ├── 2014_10_12_000000_create_users_table.php
    └── 2014_10_12_100000_create_password_resets_table.php
$ php artisan migrate

# ルーティング

routes.phpを以下のように編集します。

// app/Http/routes.php

Route::get('/', function () {
    return view('welcome');
});

// ホーム(ログインしていないと見れないよう auth middleware を適用)
Route::group(['middleware' => 'auth'], function() {
    Route::get('/home', function () {
        return view('home');
    });
});

// ログイン
Route::get('auth/login', 'Auth\AuthController@getLogin');
Route::post('auth/login', 'Auth\AuthController@postLogin');
Route::get('auth/logout', 'Auth\AuthController@getLogout');

// ユーザー登録
Route::get('auth/register', 'Auth\AuthController@getRegister');
Route::post('auth/register', 'Auth\AuthController@postRegister');

artisanコマンドで確認します。

php artisan route:list
+--------+----------+----------------------+------+-------------------------------------------------------+------------+
| Domain | Method   | URI                  | Name | Action                                                | Middleware |
+--------+----------+----------------------+------+-------------------------------------------------------+------------+
|        | GET|HEAD | /                    |      | Closure                                               |            |
|        | GET|HEAD | home                 |      | Closure                                               | auth       |
|        | GET|HEAD | auth/login           |      | App\Http\Controllers\Auth\AuthController@getLogin     | guest      |
|        | POST     | auth/login           |      | App\Http\Controllers\Auth\AuthController@postLogin    | guest      |
|        | GET|HEAD | auth/logout          |      | App\Http\Controllers\Auth\AuthController@getLogout    |            |
|        | GET|HEAD | auth/register        |      | App\Http\Controllers\Auth\AuthController@getRegister  | guest      |
|        | POST     | auth/register        |      | App\Http\Controllers\Auth\AuthController@postRegister | guest      |
+--------+----------+----------------------+------+-------------------------------------------------------+------------+

# ビュー

以下のビューを作成します。

レイアウト

{{-- resources/views/layout.blade.php --}}

<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>認証のカスタマイズ例</title>

    {{-- Bootstrap CDN --}}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
<body>

@include('navbar')

<div class="container">
    {{-- フラッシュメッセージの表示 --}}
    @if (Session::has('flash_message'))
        <div class="alert alert-success">
            {{ Session::get('flash_message') }}
        </div>
    @endif

    {{-- コンテンツの表示 --}}
    @yield('content')
</div>

{{-- jQuery & Bootstrap CDN --}}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

ナビゲーションメニュー

{{-- resources/views/navbar.blade.php --}}

<nav class="navbar navbar-default">
    <div class="container">
        <div class="navbar-header">
            <!-- スマホやタブレットで表示した時のメニューボタン -->
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>

            <!-- ブランド表示 -->
            <a class="navbar-brand" href="/home">認証のカスタマイズ</a>
        </div>

        <!-- メニュー -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <!-- 左寄せメニュー -->
            <ul class="nav navbar-nav">
                <li><a href="/home">Home</a></li>
            </ul>

            <!-- 右寄せメニュー -->
            <ul class="nav navbar-nav navbar-right">
                @if(Auth::guest())
                    <li><a href="/auth/login">Login</a></li>
                    <li><a href="/auth/register">Register</a></li>
                    @else
                            <!-- ドロップダウンメニュー -->
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                            {{ Auth::user()->name }}
                            <span class="caret"></span>
                        </a>
                        <ul class="dropdown-menu" role="menu">
                            <li><a href="/auth/logout">Logout</a></li>
                        </ul>
                    </li>
                @endif
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

ホーム

{{-- resources/views/home.blade.php --}}

@extends('layout')

@section('content')
    <h1>Home</h1>

    <p>ログイン後に表示される画面です。</p>
@endsection

ログイン

{{-- resources/views/auth/login.blade.php --}}

@extends('layout')

@section('content')
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Login</div>
                    <div class="panel-body">
                        @if (count($errors) > 0)
                            <div class="alert alert-danger">
                                <strong>Whoops!</strong> There were some problems with your input.<br><br>
                                <ul>
                                    @foreach ($errors->all() as $error)
                                        <li>{{ $error }}</li>
                                    @endforeach
                                </ul>
                            </div>
                        @endif

                        <form class="form-horizontal" role="form" method="POST" action="/auth/login">
                            <input type="hidden" name="_token" value="{{ csrf_token() }}">

                            <div class="form-group">
                                <label class="col-md-4 control-label">E-Mail Address</label>
                                <div class="col-md-6">
                                    <input type="email" class="form-control" name="email" value="{{ old('email') }}">
                                </div>
                            </div>

                            <div class="form-group">
                                <label class="col-md-4 control-label">Password</label>
                                <div class="col-md-6">
                                    <input type="password" class="form-control" name="password">
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <div class="checkbox">
                                        <label>
                                            <input type="checkbox" name="remember"> Remember Me
                                        </label>
                                    </div>
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary" style="margin-right: 15px;">
                                        Login
                                    </button>

                                    <a href="/password/email">Forgot Your Password?</a>
                                </div>
                            </div>
                        </form>
                    </div><!-- .panel-body -->
                </div><!-- .panel -->
            </div><!-- .col -->
        </div><!-- .row -->
    </div><!-- .container-fluid -->
@endsection

ユーザー登録

{{-- resources/views/auth/register.blade.php --}}

@extends('layout')

@section('content')
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Register</div>
                    <div class="panel-body">
                        @if (count($errors) > 0)
                            <div class="alert alert-danger">
                                <strong>Whoops!</strong> There were some problems with your input.<br><br>
                                <ul>
                                    @foreach ($errors->all() as $error)
                                        <li>{{ $error }}</li>
                                    @endforeach
                                </ul>
                            </div>
                        @endif

                        <form class="form-horizontal" role="form" method="POST" action="/auth/register">
                            <input type="hidden" name="_token" value="{{ csrf_token() }}">

                            <div class="form-group">
                                <label class="col-md-4 control-label">Name</label>
                                <div class="col-md-6">
                                    <input type="text" class="form-control" name="name" value="{{ old('name') }}">
                                </div>
                            </div>

                            <div class="form-group">
                                <label class="col-md-4 control-label">E-Mail Address</label>
                                <div class="col-md-6">
                                    <input type="email" class="form-control" name="email" value="{{ old('email') }}">
                                </div>
                            </div>

                            <div class="form-group">
                                <label class="col-md-4 control-label">Password</label>
                                <div class="col-md-6">
                                    <input type="password" class="form-control" name="password">
                                </div>
                            </div>

                            <div class="form-group">
                                <label class="col-md-4 control-label">Confirm Password</label>
                                <div class="col-md-6">
                                    <input type="password" class="form-control" name="password_confirmation">
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary">
                                        Register
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div><!-- .panel-body -->
                </div><!-- .panel -->
            </div><!-- .col -->
        </div><!-- .row -->
    </div><!-- .container-fluid -->
@endsection

# ユーザー登録、ログインの動作確認

ここまでで、ユーザー登録、ログイン、ログアウトができるようになります。
ここで一旦、動作確認を行います。

  • ユーザー登録:http://localhost:8000/auth/register
  • ログアウト:メニューから http://localhost:8000/auth/logout
  • ログイン:http://localhost:8000/auth/login
  • ホーム:http://localhost:8000/home (ログインしていない時は、ログイン画面にリダイレクトされます)

確認メール送信のカスタマイズ

# マイグレーション

マイグレーションファイルを作成し、ユーザーテーブルに確認メール用の項目を追加します。

php artisan make:migration add_confirmation_token_to_users_table --table=users
<?php  // database/migrations/YYYY_MM_DD_TTTTTT_add_confirmation_token_to_users_table.php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddConfirmationTokenToUsersTable extends Migration {
	public function up()
	{
		Schema::table('users', function(Blueprint $table)
		{
			$table->string('confirmation_token')->nullable();  // 確認用トークン
			$table->timestamp('confirmed_at')->nullable();  // 確認日時
			$table->timestamp('confirmation_sent_at')->nullable();  // 確認メール送信日時
		});
	}

	public function down()
	{
		Schema::table('users', function(Blueprint $table)
		{
			$table->dropColumn([
				'confirmation_token',
				'confirmed_at',
				'confirmation_sent_at'
			]);
		});
	}
}

マイグレーションを実行します。

php artisan migrate

#モデル

Userモデルを以下のように修正します。

<?php // app/User.php
namespace App;

// ...

use Carbon\Carbon; // 追加

class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
	// ...

	protected $hidden = [
		'password', 'remember_token',
		// ① 追加
		'confirmation_token', 'confirmed_at', 'confirmation_sent_at'
	];

	protected $dates = [  // ② 追加
		'confirmed_at',
		'confirmation_sent_at',
	];

	public function makeConfirmationToken($key) { // ③ 追加
		$this->confirmation_token = hash_hmac(
			'sha256',
			str_random(40).$this->email,
			$key
		);

		return $this->confirmation_token;
	}

	public function confirm() { // ④ 追加
		$this->confirmed_at = Carbon::now();
		$this->confirmation_token = '';
	}

	public function isConfirmed() { // ⑤ 追加
		return ! empty($this->confirmed_at);
	}
}

① 念の為、確認関連の項目を隠し属性とします。

② タイムスタンプ型の項目を日付ミューテータとします。

③ makeConfirmationToken() を追加します。
confirmation_tokenを生成して、自身にセットした後に、それを返します。

④ confirm() を追加します。
確認日時をセットし、確認トークンをクリアして、ユーザー登録確認が完了した状態にします。

⑤ isConfirmed() を追加します。
ユーザー登録確認が完了しているかをチェックする為に使います。

# コントローラ

AuthController.phpを以下の様に修正します。

<?php  // app/Http/Controllers/Auth/AuthController.php

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

// 追加
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Contracts\Config\Repository as Config;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers;

    protected $redirectTo = '/home'; // ①

    public function __construct()
    {
        // 変更なし
    }

    protected function validator(array $data)
    {
        // 変更なし
    }

    /**
     * ② ユーザーの作成
     * ユーザーを作成し、確認メールを送信する
     *
     * @param Mailer $mailer
     * @param array $data
     * @param $app_key
     * @return User
     */
    protected function create(Mailer $mailer, array $data, $app_key)
    {
        $user = new User;

        $user->name = $data['name'];
        $user->email = $data['email'];
        $user->password = bcrypt($data['password']);

        $user->makeConfirmationToken($app_key);
        $user->confirmation_sent_at = Carbon::now();

        $user->save();

        $this->sendConfirmMail($mailer, $user);

        return $user;
    }

    /**
     * ③ 確認メールの送信
     *
     * @param Mailer $mailer
     * @param User $user
     */
    private function sendConfirmMail(Mailer $mailer, User $user)
    {
        $mailer->send(
            'emails.confirm',
            ['user' => $user, 'token' => $user->confirmation_token],
            function($message) use ($user) {
                $message->to($user->email, $user->name)->subject('ユーザー登録確認');
            }
        );
    }

    /**
     * ④ ユーザー登録アクション
     * バリデーションチェックを行い、ユーザーを作成する
     *
     * @param Request $request
     * @param Mailer $mailer
     * @param Config $config
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postRegister(Request $request, Mailer $mailer, Config $config)
    {
        $validator = $this->validator($request->all());

        if ($validator->fails()) {
            $this->throwValidationException(
                $request, $validator
            );
        }

        $this->create($mailer, $request->all(), $config->get('app.key'));

        \Session::flash('flash_message', 'ユーザー登録確認メールを送りました。');

        return redirect('auth/login');
    }

    /**
     * ⑤ ユーザーを確認済にする
     *
     * @param $token
     * @return \Illuminate\Http\RedirectResponse
     */
    public function getConfirm($token) {
        $user = User::where('confirmation_token', '=', $token)->first();
        if (! $user) {
            \Session::flash('flash_message', '無効なトークンです。');
            return redirect('auth/login');
        }

        $user->confirm();
        $user->save();

        \Session::flash('flash_message', 'ユーザー登録が完了しました。ログインしてください。');
        return redirect('auth/login');
    }
}

① ログイン後のリダイレクト先を /home にします。

② create() を修正
ユーザー登録時に確認用トークンと確認メール送信日時を設定します。
ユーザー登録後に確認メールを送信します。

③ 確認メールの送信を追加
確認メールにトークンを埋め込んで送信します。

④ postRegister() をオーバーライド
postRegister() のオリジナルは AuthenticatesAndRegistersUsersトレイトの中で定義されています。オリジナルはユーザーを作成した後にすぐログインするようになっています。それを、オーバーライドして、ユーザー作成後に、ログインしないように変更します。

⑤ getConfirm() を追加
確認メールに記載するURLにアクセスした時に実行されるアクションです。confirmation_tokenでユーザーを検索し、本人確認が終了した状態にします。

# ビュー

確認メールのビューとして confirm.blade.php を追加します。

{{-- resources/views/emails/confirm.blade.php --}}

<p>
	ようこそ、{{ $user['name'] }} さん
</p>

<p>
	以下のリンクをクリックしてユーザーを有効化してください。
</p>

<p>
	<a href="{{ url('auth/confirm', [$token]) }}">ユーザーを有効化する</a>
</p>

# ルーティング

// app/Http/routes.php

...

// ユーザー登録
...
Route::get('auth/confirm/{token}', 'Auth\AuthController@getConfirm'); // ①

① ユーザー確認処理へのルートを追加

artisanコマンドで確認します。

php artisan route:list
+--------+----------+----------------------+------+-------------------------------------------------------+------------+
| Domain | Method   | URI                  | Name | Action                                                | Middleware |
+--------+----------+----------------------+------+-------------------------------------------------------+------------+
|        | GET|HEAD | /                    |      | Closure                                               |            |
|        | GET|HEAD | home                 |      | Closure                                               | auth       |
|        | GET|HEAD | auth/login           |      | App\Http\Controllers\Auth\AuthController@getLogin     | guest      |
|        | POST     | auth/login           |      | App\Http\Controllers\Auth\AuthController@postLogin    | guest      |
|        | GET|HEAD | auth/logout          |      | App\Http\Controllers\Auth\AuthController@getLogout    |            |
|        | GET|HEAD | auth/register        |      | App\Http\Controllers\Auth\AuthController@getRegister  | guest      |
|        | POST     | auth/register        |      | App\Http\Controllers\Auth\AuthController@postRegister | guest      |
|        | GET|HEAD | auth/confirm/{token} |      | App\Http\Controllers\Auth\AuthController@getConfirm   | guest      |
+--------+----------+----------------------+------+-------------------------------------------------------+------------+

前編のまとめ

だいぶ長くなってきたので、一旦ここで終了し、残りは次回の【後編】で行います。

今回のカスタマイズで、以下のことができるようになりました。
動作確認をしてみてください。

  • ユーザー登録直後に確認メールを送信
  • 確認メール内のURLにアクセスして、ユーザー確認を完了

後編の内容

  • 確認メールからのユーザー確認が済んでいない時、ログイン出来ないようにする。
  • 確認メールの再送を可能にする。

【後編】はこちら

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中