はじめに
デフォルトで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',
]);