Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【Laravel7でユーザー認証_7】会員登録時にメール認証を行う

はじめに

Laravelのユーザー認証機能を利用する際、会員登録時にメール認証を挟む手順をまとめます。

環境

XAMPP環境でLaravelが使えるように設定してあります。

  • Windows10 Pro 64bit
  • PHP 7.3.18
  • Laravel 7.12.0
  • MariaDB 10.1.32

また、Laravelプロジェクトは以下の手順で作業を進めており、ユーザーの情報を表示・変更する画面が表示できるまでできていること前提の手順となっています。
また、.env で SMTPサービスを設定して、メールも送信できるようにしています。

実装手順

Usersモデルの設定

Usersモデルについて、MustVerifyEmail を実装するように変更します。

app/User.php
- class User extends Authenticatable
+ class User extends Authenticatable implements MustVerifyEmail
  {

ルーティングの設定

Auth::routes にオプションを渡し、メール認証を有効にします。

routes/web.php
- Auth::routes();
+ Auth::routes(['verify' => true]);

このままだと、認証メールは届きますが、メールを無視してもログインができてしまう状態です。
メールアドレス確認済みのユーザーのみアクセスできるように、アクセスを制限したいルーティングについて ->middleware('verified') を追加します。

試しに、homeにだけつけてみます。

routes/web.php
- Route::get('/home', 'HomeController@index')->name('home');
+ Route::get('/home', 'HomeController@index')->name('home')->middleware('verified');

一旦確認

ユーザー登録をして、メールが届くかどうか、認証前にログインするとhomeの表示が変わるかどうか確認します。

▼届くメール

▼メール認証せずにログインすると表示される画面

日本語化

認証メールや表示される画面が一部日本語化されていないので、修正します。
※詳しくは 【Laravel7でユーザー認証_2】ユーザー認証を日本語化
ちなみに、認証メールの本文やSubjectはvender/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php にかかれています。

resources/views/auth/verify.blade.php
                      {{ __('Before proceeding, please check your email for a verification link.') }}
-                     {{ __('If you did not receive the email') }},
+                     {{ __('If you did not receive the email,') }}
                      <form class="d-inline" method="POST" action="{{ route('verification.resend') }}">
                          @csrf
-                         <button type="submit" class="btn btn-link p-0 m-0 align-baseline">{{ __('click here to request another') }}</button>
+                         <button type="submit" class="btn btn-link p-0 m-0 align-baseline">{{ __('click here to request another.') }}</button>
                      </form>
resources/ja.json
+     "Verify Email Address": "メールアドレス認証",
+     "Please click the button below to verify your email address.": "下のボタンをクリックして認証してください。",
+     "If you did not create an account, no further action is required.": "このメールにお心当たりがない方は、恐れ入りますが、このメールを削除してください。"
  }

ルーティングを修正

認証前ユーザーは、/settingで自分の登録情報を参照することだけはできるけれど、書き換えには認証が必要という状態になるように修正します。

HTTPプロトコル URI ゲスト 認証前ユーザー 認証済ユーザー
GET /
GET /login
GET /home ×(/login)
GET /setting ×(/login)
GET,POST /setting/name ×(/login) ×(/email/verify)
GET,POST /setting/email ×(/login) ×(/email/verify)
GET,POST /setting/username ×(/login) ×(/email/verify)
GET,POST /setting/password ×(/login) ×(/email/verify)
GET,POST /setting/deactive ×(/login)

先ほどは、app/web.php->middleware('verified') を指定しましたが、各コントローラのコンストラクタに書くこともできます。
どちらも一長一短あるかと思いますが、今回はコンストラクタに記述することにします。

Settingコントローラでは、indexはメール認証無しでアクセス可能という仕様なので、->except('index'); をつけてverifiedから除外します。

app/Http/Controllers/SettingController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified')->except('index');
      }
app/Http/Controllers/Auth/ChangePasswordController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified');
      }
app/Http/Controllers/Auth/ChangePasswordController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified');
      }
routes/web.php
- Route::get('/home', 'HomeController@index')->name('home')->middleware('verified');
+ Route::get('/home', 'HomeController@index')->name('home');

動作確認

/setting の画面は認証前ユーザーでも表示され、名前やメールアドレスの変更フォームは認証してください画面へリダイレクトする仕様が実現できました。

しかし、クリックしないと操作できるかできないかがわからないのでは不親切です。
viewを変更して、もう少し親切設計にしたいと思います。

viewの修正

未認証のユーザーの場合、/setting画面で「メール認証をしてください」のアラートを表示させ、ユーザー情報変更の画面へのリンクは消しておく、という仕様に変更します。

メール認証済ユーザーの場合、データベース上は email_verified_at のカラムに日付が入り、そうでないユーザーはNULLとなります。
これを利用して、ログインユーザーのemail_verified_at がNULLかどうかで処理を分けました。
(書き換える内容が多かったので、viewファイル自体を分けてしまった方がよかったのかもしれないですね…)

resources/views/setting/index.blade.php
                  <div class="card-body">
                      @if (session('status'))
                          <div class="alert alert-success" role="alert">
                              {{ session('status') }}
                          </div>
                      @endif
+  
+                     @if (!$auth->email_verified_at)
+                         <div class="alert alert-warning" role="alert">
+                           {{ __('Before proceeding, please check your email for a verification link.') }}
+                           {{ __('If you did not receive the email,') }}
+                           <form class="d-inline" method="POST" action="{{ route('verification.resend') }}">
+                               @csrf
+                               <button type="submit" class="btn btn-link p-0 m-0 align-baseline">{{ __('click here to request another.') }}</button>
+                           </form>
+                         </div>
+                     @endif

                      <div class="list-group mb-3" style="max-width:400px; margin:auto;">
-                       <a href="{{ route('name.form') }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
+                       <a href="{{ route('name.form') }}"
+                       @if ($auth->email_verified_at)
+                         class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
+                       @else
+                          class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled" tabindex="-1" aria-disabled="true"
+                       @endif
+                       >
                          <dl class="mb-0">
                            <dt>{{ __('Name') }}</dt>
                            <dd class="mb-0">{{ $auth->name }}</dd>
                          </dl>
+                         @if ($auth->email_verified_at)
                          <div><i class="fas fa-chevron-right"></i></div>
+                         @endif
                        </a>

-                       <a href="{{ route('email.form') }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
+                       <a href="{{ route('email.form') }}"
+                       @if ($auth->email_verified_at)
+                         class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
+                       @else
+                          class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled" tabindex="-1" aria-disabled="true"
+                       @endif
+                       >
                          <dl class="mb-0">
                            <dt>{{ __('E-Mail Address') }}</dt>
                            <dd class="mb-0">{{ $auth->email }}</dd>
                          </dl>
+                         @if ($auth->email_verified_at)
                          <div><i class="fas fa-chevron-right"></i></div>
+                         @endif
                        </a>

-                       <a href="{{ route('password.form') }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
+                       <a href="{{ route('password.form') }}"
+                       @if ($auth->email_verified_at)
+                         class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
+                       @else
+                          class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled" tabindex="-1" aria-disabled="true"
+                       @endif
+                       >
                          <dl class="mb-0">
                            <dt>{{ __('Password') }}</dt>
                            <dd class="mb-0">********</dd>
                          </dl>
+                         @if ($auth->email_verified_at)
                          <div><i class="fas fa-chevron-right"></i></div>
+                         @endif
                        </a>
                  </div>

↓メール認証前

↓メール認証後

おわりに

これで会員登録時にメール認証機能をもたせることができました。
ただ、このままだと有効なメールアドレスで登録・メール認証した後、不正なメールアドレスに変更ができてしまいます。
メールアドレス変更時にもメール認証をするようにアップデートしなければならなそうです。
次回はその部分を調べながら実装したいと思います。

参考サイト

crosawassant
フレームワーク(今はLaravel)の勉強中です。 おかしなところやセキュリティ的にまずいところ、もっと効率がよい方法などありましたら、ぜひコメントください。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away