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


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

# 環境

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

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

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

- [【Laravel7でユーザー認証_1】基本のき](https://qiita.com/crosawassant/items/bf3982666b6333bcb8c8)
- [【Laravel7でユーザー認証_2】ユーザー認証を日本語化](https://qiita.com/crosawassant/items/d1f4c5c93f30afa9eb65)
- [【Laravel7でユーザー認証_3】ユーザー認証をメールアドレスからユーザー名に変更する](https://qiita.com/crosawassant/items/2464db8a0fe1c603401a)
- [【Laravel7でユーザー認証_4】パスワード変更フォームを作成する](https://qiita.com/crosawassant/items/04242c8d7a12346dcc20)
- [【Laravel7でユーザー認証_5】ユーザーを倫理削除（SoftDelete）する](https://qiita.com/crosawassant/items/a6e3e9b1aedf566b2d27)
- [【Laravel7でユーザー認証_6】ユーザーの情報を表示・変更する設定画面を作成する](https://qiita.com/crosawassant/items/d8b434f0bc98455165b4)

# 実装手順

##Usersモデルの設定

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

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

##ルーティングの設定

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

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

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

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

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

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

▼届くメール
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262236/c5b87e3e-ddbf-d1ed-19c2-e241b411fe91.png">

▼メール認証せずにログインすると表示される画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262236/c91080f8-fb8d-0144-d6a7-c26a259a644d.png" width="600">

##日本語化

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

```diff: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>
```

```diff: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から除外します。

```diff:app/Http/Controllers/SettingController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified')->except('index');
      }
```
```diff:app/Http/Controllers/Auth/ChangePasswordController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified');
      }
```
```diff:app/Http/Controllers/Auth/ChangePasswordController.php
      public function __construct()
      {
          $this->middleware('auth');
+         $this->middleware('verified');
      }
```
```diff: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ファイル自体を分けてしまった方がよかったのかもしれないですね…）

```diff: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>
```
↓メール認証前
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262236/b842ad1c-b52a-184f-7b0b-b0c2bd7a298a.png">
↓メール認証後
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262236/4bc1e8a5-30cc-a5ae-98e5-7fcaecb86c83.png">

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

#参考サイト
- [Laravel 7.x メール確認](https://readouble.com/laravel/7.x/ja/verification.html)
- [Laravel：Email Verification（メールアドレスの確認）を使う](https://qiita.com/usaginooheso/items/d965f95191739f9335e8)
- [[Laravel]メール認証を使った会員登録](https://qiita.com/nitaking/items/588a645968efcac6d37a)
- [【Laravel】メールアドレスを変更する方法を徹底解説【コピペでOK】](https://yaba-blog.com/laravel-email-chenge/)