13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravel9でログイン時にメールアドレスではなくユーザ名で認証をする

Posted at

はじめに

デフォルトでLaravelはメールアドレスemailを認証に利用する。過去にLaravel5.8で、ログイン時のユーザ認証にメールアドレスではなくユーザ名usernameを利用するようにカスタマイズしたが、その時は、以下のページの"Username Customization"の項を参照にした。

Laravel9で、同じようにユーザ名で認証を行うようにカスタマイズしようとしたが、Laravel9のドキュメントでは正にこの目的のためのガイドが見つけられず、ドキュメントとコードを調べながら試行錯誤して実現したので、その結果をメモする。

前提

カスタマイズをする以前に、ユーザ認証機能そのものの導入手段が、Laravel5.8と9では異なっており、Laravel9では、以下のページをみて、Laravel Breezaeをインストールすることで導入した。

この時の、各バージョンは次のとおり。

"name": "laravel/framework", "version": "v9.0.2",
"name": "laravel/breeze",    "version": "v1.8.1",

本稿は、Laravel9とLaravel Breezaeが導入済みであり、メールアドレスでの認証はできている状態を前提とする。

やったこと

まずは見た目から... フォームでの入力を「名前」にする

「ログイン」フォームの、1番目のテキストボックスのキャプションがEmailとなっているので、これをNameに修正する。
1番目のテキストボックスに入力した値(value)が、このフォームからPOST /loginで送信するデータ(リクエスト)に含まれるが、その時のkeyが、emailではなくnameになるように修正する。

--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -15,11 +15,11 @@
         <form method="POST" action="{{ route('login') }}">
             @csrf
 
-            <!-- Email Address -->
+            <!-- Name -->
             <div>
-                <x-label for="email" :value="__('Email')" />
+                <x-label for="name" :value="__('Name')" />
 
-                <x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
+                <x-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus />
             </div>
 
             <!-- Password -->

リクエストの検証ルールを変更する

POST /loginにアクセスすると、AuthenticatedSessionControllerクラスのstoreメソッドを実行するが、storeメソッドにはLoginRequest型のデータが必要になる。

$ php artisan route:list
...
  GET|HEAD   login ........ login › Auth\AuthenticatedSessionController@create
  POST       login ................ Auth\AuthenticatedSessionController@store
  POST       logout ...... logout › Auth\AuthenticatedSessionController@destroy
class AuthenticatedSessionController extends Controller
{
...
    public function store(LoginRequest $request)
    {
        $request->authenticate();
...

「ログイン」フォームから送信するデータから、LoginRequestが生成されstoreメソッドに渡されるが、このLoginRequestを生成するときの検証ルールが定義されており、デフォルトでは"emailが必要"となっているため"nameが必要"に修正する。

前項の修正によって、「ログイン」フォームから送信するデータにはemailをkeyにするデータは含まれなくなっているため、この検証ルールの変更をしないと、検証に失敗し、storeメソッドを実行する前にエラーとなってしまう。

--- a/app/Http/Requests/Auth/LoginRequest.php
+++ b/app/Http/Requests/Auth/LoginRequest.php
@@ -29,7 +29,7 @@ public function authorize()
     public function rules()
     {
         return [
-            'email' => ['required', 'string', 'email'],
+            'name' => ['required', 'string'],
             'password' => ['required', 'string'],
         ];
     }

認証試行の引数にユーザ名を渡す

これが、今回の修正のメイン。

storeメソッドが実行されると、認証のためにLoginRequestクラスのauthenticateメソッドを実行する。authenticateメソッドでは、Laravelの認証サービスを利用して実際の認証を行うため、Authファサードのattemptメソッドを実行する。

attemptメソッドは第一引数に指定された連想配列(key/valueのペア)を使って、データベースのusersテーブルからユーザを探し、ユーザが見つかったら配列のpasswordの値をハッシュしたものと、データベースにある既にハッシュ化されたパスワードを比較する。

attemptメソッドに指定する連想配列のkeyがユーザを探すときのusersテーブルのカラムになる。デフォルトではemailカラムの値が、emailをkeyとする値と一致するユーザを探すが、指定する連想配列を修正し、nameカラムが、nameをkeyとする値(=ログインフォームで入力された値)と一致するユーザを探すようにする。

--- a/app/Http/Requests/Auth/LoginRequest.php
+++ b/app/Http/Requests/Auth/LoginRequest.php
@@ -45,11 +45,11 @@ public function authenticate()
     {
         $this->ensureIsNotRateLimited();
 
-        if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
+        if (! Auth::attempt($this->only('name', 'password'), $this->boolean('remember'))) {
             RateLimiter::hit($this->throttleKey());
 
             throw ValidationException::withMessages([
                 'email' => trans('auth.failed'),
             ]);
         }

attemptメソッドの使い方については、以下のページを参照した。

ユーザ登録時にユーザ名が重複していないかチェックする

認証するためにユーザ名を使用するので、同じ名前のユーザが複数いるとユーザ名でユーザを特定できなくなってしまう。メールアドレスで認証をするときは、メールアドレスが重複しなければよかったが、ユーザ名で認証することにしたため、ユーザ名も重複しないように修正する。
ユーザ登録におけるリクエストの検証ルールに、ユーザ名がユニークであることを追加する。またデータベースの定義でもnameカラムをユニークするための属性をつける。

--- a/book-keeping/app/Http/Controllers/Auth/RegisteredUserController.php
+++ b/book-keeping/app/Http/Controllers/Auth/RegisteredUserController.php
@@ -34,7 +34,7 @@ public function create()
     public function store(Request $request)
     {
         $request->validate([
-            'name' => ['required', 'string', 'max:255'],
+            'name' => ['required', 'string', 'max:255', 'unique:users'],
             'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
             'password' => ['required', 'confirmed', Rules\Password::defaults()],
         ]);
--- a/book-keeping/database/migrations/2014_10_12_000000_create_users_table.php
+++ b/book-keeping/database/migrations/2014_10_12_000000_create_users_table.php
@@ -15,7 +15,7 @@ public function up()
     {
         Schema::create('users', function (Blueprint $table) {
             $table->id();
-            $table->string('name');
+            $table->string('name')->unique();
             $table->string('email')->unique();
             $table->timestamp('email_verified_at')->nullable();
             $table->string('password');

エラーの項目を「名前」にする

認証に失敗したときや、何回も続けて失敗した時には、例外を発生させ、エラーメッセージを「ログイン」フォームに表示する。
ValidationException::withMessages()に引数で指定する配列が、エラーメッセージ($errors)の内容になる。配列のkeyがエラーの項目(何に関するエラーか)になり、値がメッセージそのものになる。

@props(['errors'])

@if ($errors->any())
    <div {{ $attributes }}>
        <div class="font-medium text-red-600">
            {{ __('Whoops! Something went wrong.') }}
        </div>

        <ul class="mt-3 list-disc list-inside text-sm text-red-600">
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

ユーザ名で認証をするのに伴って、エラーメッセージもemailではなくnameに関するものになるので、ValidationException::withMessages()に指定する配列のkeyを修正する。
現状、「ログイン」フォームで表示するエラーメッセージは、エラーの項目を気にせず$errors->all()で文字列を出力しているため、直接的な効果がないが、意味(何に関するエラーか)が変わるのに合わせて修正する。

--- a/book-keeping/app/Http/Requests/Auth/LoginRequest.php
+++ b/book-keeping/app/Http/Requests/Auth/LoginRequest.php
@@ -45,11 +45,11 @@ public function authenticate()
     {
         $this->ensureIsNotRateLimited();
 
         if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
             RateLimiter::hit($this->throttleKey());
 
             throw ValidationException::withMessages([
-                'email' => trans('auth.failed'),
+                'name' => trans('auth.failed'),
             ]);
         }
 
@@ -74,7 +74,7 @@ public function ensureIsNotRateLimited()
         $seconds = RateLimiter::availableIn($this->throttleKey());
 
         throw ValidationException::withMessages([
-            'email' => trans('auth.throttle', [
+            'name' => trans('auth.throttle', [
                 'seconds' => $seconds,
                 'minutes' => ceil($seconds / 60),
             ]),

エラーメッセージや、@errorsについては以下のページを参照した。

失敗の回数を数えるときのキーをユーザ名との組み合わせで生成する

同じリクエストが、短期間に連続して認証に失敗する場合、しばらくの間、ログインを禁止する(強制的に認証を失敗にする)。"同じリクエスト"と見做すためのキーはthrottleKey()で生成される。デフォルトではリクエストに含まれるメールアドレスとIPアドレスの組み合わせでキーを生成していたが、これをユーザ名とIPアドレスの組み合わせで生成するように修正する。
ユーザ名で認証することにしたため、「ログイン」フォームからのリクエストにはメールアドレスは含まれず、この修正をしないと、IPアドレスだけでキーを生成することになる。その結果、あるユーザが規定の回数認証に失敗すると、同じIPアドレスからリクエストを送っている、別のユーザもログイン禁止の条件に該当したと誤って判定してしまう。

--- a/book-keeping/app/Http/Requests/Auth/LoginRequest.php
+++ b/book-keeping/app/Http/Requests/Auth/LoginRequest.php
@@ -88,6 +88,6 @@ public function ensureIsNotRateLimited()
      */
     public function throttleKey()
     {
-        return Str::lower($this->input('email')).'|'.$this->ip();
+        return Str::lower($this->input('name')).'|'.$this->ip();
     }
 }

おまけ: テストケースの修正

Laravel Breezaeをインストールすると、認証機能に関するテストケースが追加される。POST /loginされた時の振る舞いのテストでは、デフォルトの「ログイン」フォームに合わせて、メールアドレスとパスワードをインプットしているが、今回の修正に合わせて、ユーザ名とパスワードをインプットするように修正する。

--- a/book-keeping/tests/Feature/Auth/AuthenticationTest.php
+++ b/book-keeping/tests/Feature/Auth/AuthenticationTest.php
@@ -23,7 +23,7 @@ public function test_users_can_authenticate_using_the_login_screen()
         $user = User::factory()->create();
 
         $response = $this->post('/login', [
-            'email' => $user->email,
+            'name' => $user->name,
             'password' => 'password',
         ]);
 
@@ -36,7 +36,7 @@ public function test_users_can_not_authenticate_with_invalid_password()
         $user = User::factory()->create();
 
         $this->post('/login', [
-            'email' => $user->email,
+            'name' => $user->name,
             'password' => 'wrong-password',
         ]);
13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?