【Laravel12】自作モデルでログイン認証！〜パッケージ追加なしでログイン機能を実装してみた〜

## はじめに
### 本記事のポイント
#### 知りたいであろう情報
本記事では、Laravelにおける自作モデルでのログイン認証方法を説明します。
ルーティング記述からメソッド実装まで、幅広く記していますが、おそらく**ここに辿り着いた方の多くが知りたいことは「Authファザードと自作モデルの連携」だと思います**。そこだけ知れば十分という方は、目次等から「[認証モデルの設定](#認証モデルの設定)」へ遷移してお読みください。

#### 認証モデルを追加したい方へ
本記事では、**自作モデルを認証モデルのデフォルトとする**ことをメインにしています。
**認証モデルを追加したいという方は、先に「[認証モデルの追加](#認証モデルの追加)」をお読みください。**

### 前提
- Laravelで自作モデルを作成した・できる
- **LaravelのSSR機能を用いる(ログインAPIとしては利用しない)**

本記事では、自作モデルを使ったログイン認証について説明します。
自作モデルをの作り方が分からない方は、モデルの作成方法を理解してから臨むことをお奨めします。

また、ここでは**LaravelをAPIサーバとして利用することを想定していません**。LaravelをAPIサーバとして活用する場合、`Laravel Sanctum`パッケージのインストールが必要になります。

https://laravel.com/docs/12.x/sanctum

この場合もログイン認証の方法は変わらないとは思いますが、前提としてその保証をすることができません。
そのため、LaravelをAPIサーバとして開発される方は、あくまでも参考程度に目を通していただければと思います。

なお、本記事は以下の記事からの続きになります。ご興味のある方はぜひご一読ください。

- 環境構築

https://qiita.com/kinoha/items/4cd216d69877846345ef

- モデル作成

https://qiita.com/kinoha/items/1e1cb1c9741fa33b76c0

## 本記事のゴール
- **追加パッケージやスターターキットを使わずにログイン認証をすること**
- **自作モデルを利用したログイン認証をすること**

Laravelにはログイン認証を行うための機能が備わっています。しかし、デフォルトでは`User`モデル（デフォルトで作られるモデル）に対する認証機能となってしまいます。
つまり、**デフォルトでは自作モデルを用いたログイン実装ができない**のです。初期状態では自作モデルなんて存在しないわけですから、当然ではありますね。

また、chatGPTや先人の知恵を借りると「BreezeやJetstreamなどを使う」というように、追加パッケージを利用する方法が多々紹介されてる印象です。
もちろんこの方法がベストプラクティスだと思います。一方で、自作モデルでの認証は結局できずに終わったり、パッケージインストール時に訳も分からずディレクトリが構築されて訳が分からなくなったり…
という風に何度もプロジェクトを破棄したのが私になります。

本記事では、**新たにパッケージやキットをインストールすることなく、自作モデルでログイン認証を行えるようにする**ことを目指します。

なお、今回も公式ドキュメントが非常に役立ちます。この記事よりも何よりも正確な情報となりますので、ぜひドキュメントを開きながら実装してみてください。

https://laravel.com/docs/12.x/authentication

## 認証モデルの作成
ログイン認証に利用したいモデルを作成します。本記事では、他記事で作成した`Member`モデルを用いることにします。
`Member`モデルの構成は以下のとおりです。
| カラム名 | 型 | 備考|
|:------:|:--:|:---:|
| id | BigInteger | 主キー |
| user_code | string | ログインIDとして利用，自動生成，ユニーク制約有り |
| user_name | string | 表示ユーザ名，ユーザが自由に設定可 |
| password | string | パスワード，ハッシュ化して保持 |
| email | string | メールアドレス |
| email_verified_at | timestamp | メール認証日時の保持 |

モデルの作成方法は、「はじめに」のモデル作成記事をご覧ください。

## ルーティング（パス）・ビュー（ページ）作成
### ビューの作成
ログイン画面・ログイン後画面の作成をします。
なお、**LaravelをAPIサーバとして利用する場合はこの工程が不要**となると思います。SPA開発をしている場合は、まさにこれに該当すると思います。

`resources/views`ディレクトリに、以下に当たるファイルを作成してください。ファイル名は問いません。あくまでも「本記事における便宜上のファイル名」となります。

- `login.blade.php`：ログインページ
- `dashboard.blade.php`：ダッシュボード（ログイン後）ページ

基本的には`HTML`と同じように記述ができます。お好きなログインページ・ダッシュボード等ログイン後ページを作成してください。

ただし、**ログイン画面には以下の要素を必ず入れてください**。
```html:login.blade.php
<form action="{{ route('login') }}" method="POST">
    @csrf
    <input type="text" name="user_code">
    <input type="password" name="password">
    <button type="submit">（お好きなテキスト）</button>
</form>
```

---

Bladeテンプレートを用いて、各画面にあたるテンプレートを作成します。特段言うべきことはありません。**ログイン用のデータを送信できるようなフォームを作成してください**。

#### フォームの送信先（`action`属性）
以下のようにすることも可能です。
```html:formタグ
<!-- パス直打ち -->
<form action="/login" method="POST">

<!-- urlメソッドを利用した記述 -->
<form action="{{ url('login') }}" method="POST">
<form action="{{ url('/login') }}" method="POST">
```
何でもありな気がしてきますね。

Laravelには、今回のようにビューファイル内で「決まり切ったURL」を指定するだけでも、ザッと以下のような方法があります。
- パス直打ち
HTMLをやっていればお馴染みの方法。
- `routeメソッド`
後述の`ルーティング：名前付きルート`に関連した方法。**引数に指定された名前を持つルートへのパス**が返されます。
- `urlメソッド`
変数なども用いることができるURL生成方法。GETパラメータを埋め込みたいようなときに利用。

今回は、`route`メソッドを利用しました。この利点は、**パスを変えても対応がしやすい**ということです。どんなパスであろうとも、目的のルートに付けられた名前を変えなければそこのパスに繋がります。
ここで実装する認証から、さらにログイン認証を行うモデルを増やすとしても、それに合わせてルートに名前を付ければ「後でパスを変えてもビューファイルを変えなくても大丈夫」になります。

とりあえず、この章では「そういうもの」として扱っていただければ大丈夫です。詳細は以下公式ドキュメントをお読みください。

https://laravel.com/docs/12.x/urls#main-content

### ルーティング作成
ログインフォームページ・フォーム送信先・ログイン成功後ページのそれぞれのパスを定義します。
```php:routes/web.php
use App\Http\Controllers\Auth\LoginController; // コントローラのインポート

// ログインフォームページ
Route::get('/login', function () {
    return view('login'); // 引数：ログイン画面用のビューファイル名
})->name('login');

// フォーム送信先
Route::post('/login', [LoginController::class, 'authenticate']);

// ログイン成功後ページ
Route::get('/dashboard', function () {
    return view('dashboard'); // 引数：ログイン後画面用のビューファイル名
})->name('dashboard');
```

---

ここでは「そもそもどこでログインを行うのか」「ログインしたらどこのページに飛ぶのか」を定義しています。

#### ルーティングとは

https://laravel.com/docs/12.x/routing

`ルーティング`とは、**アクセスされたパスと実際の処理を関連付ける**仕組みです。今回で言えば、ブラウザで`http://~~~/login`にアクセスされたら`Route::get('/login', function () {...});`という処理が実行される、というイメージです。
**第１引数と一致したパスにアクセスしたら、第２引数に示した処理が実行される**と言った方がより正確かもしれません。ここに、さらにGetやらPostやらのHTTPメソッドの比較も入ってきます。ちなみに、大雑把には、以下のようなHTTPメソッドがあります。

- GET：通常のページアクセス
- POST：フォーム等データの送信
- PUT(PATCH)：オブジェクトデータの更新（アカウント情報更新とか）
- DELETE：オブジェクトデータの削除（アカウント削除とか）

現段階で意識すべきなのはGET，POSTになります。とりあえず、**ただページにアクセスするだけのパスならGET、フォームデータの送信先になるパスならPOST**と割り切っておけば大丈夫です。

MVCモデルの挙動説明、及び、各HTTPメソッドの意義については本記事の範疇を超えるため、割愛します。

#### 本記事のルーティング
本記事では、ログインページとログイン認証を行うためのパスを、**どちらも`/login`としています**。そして、それぞれの区別は**HTTPメソッドが`GET`と`POST`かどうか**で行っています。

さらに、GETメソッド（→ログインページ表示）側のルートには、**`name`メソッドにより`login`という名前を付けています**。これにより、`{{ route('login') }}`というビューメソッドへの記述をすると`/login`というパスが返されるようになります。

https://laravel.com/docs/12.x/routing#named-routes:~:text=last%20route%20segment.-,Named%20Routes,-Named%20routes%20allow

「GETメソッド側にしか名前を付けていないのに、どうしてPOSTメソッド側にフォームを送ることができるのか」という声も聞こえてきそうです。これは、**`GET`も`POST`もパスが同じだから**です。
「じゃあ両方に同じ名前を付ければ良い」という声も聞こえてきそうです。これは、**ルートの名前にはユニーク制約がかかっているためNG**です。
Laravelの`route('name')`は、ルーティングの中から`name`が名付けられているルートを探します。**これはHTTPメソッドによる区別を行いません**。ただ名前を比較し、一致すればそのパスを返すのみです。
ログイン認証モデルによってパスを固定してルートを通し、それぞれに区別できるような名前を付けるような運用方法が安全だと思われます。

## 認証メソッドの実装
今回は先に公式ドキュメントを共有します。
「今からこんな感じのメソッドを実装するんだな」と眺めてみてください。

https://laravel.com/docs/12.x/authentication#authenticating-users

### コントローラファイルの作成
ログイン認証を行うためのコントローラ`LoginController`を作成します。
```:bash
php artisan make:controller Auth/LoginController
```
実行後、`app/Http/Controllers/Auth/LoginController.php`が作成されていることを確認してください。

---

ログインは、内部的には以下のような流れで行います。
1. ログインページにアクセスする（Get /login）
1. ログインページからフォームを送信する（Post /login）
1. 送信されたフォームデータでログイン認証を行う（**←コントローラで処理**）

ここでは、実際にログイン認証を実行するための「コントローラ」を作成します。
Laravelでは以下のコマンドでコントローラを作成することができます。
```:bash
php artisan make:controller HogeController
```

基本的にはこれだけで良いのですが、今回は`Auth/LoginController`と、如何にも認証用っぽい文字列を入れました。**これはあってもなくても問題ないです**。
通常では、`LoginController`というコントローラ名だけ書けばOKです。ただ、**コントローラファイルを配置したいディレクトリにこだわりがあれば**、今回のように`Fuga/HogeController`のように、作成先のディレクトリを指定することができます。

今回は「認証周りに使うコントローラは区別しておきたい」という気持ちがあったので、`Auth`ディレクトリに`LoginController`を配置しました。直にコントローラファイルを置きたい場合や、また別の名前のディレクトリに入れたいという場合は、**ルーティングでのコントローラをインポートするパスを間違えないように修正してください**。

```php:web.php
use App\Http\Controllers\LoginController // Controllersディレクトリ直下

use App\Http\Controllers\Members\LoginController // Membersディレクトリ内

...
```

### 認証メソッドの実装
フォームから送られたデータを基に認証を行うメソッドを実装します。

```php:LoginController
use Illuminate\Http\RedirectResponse; // 追記
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; //追記

class LoginController extends Controller
{
    public function authenticate(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'user_code' => ['required', 'string'],
            'password' => ['required', 'string']
        ]);

        if(Auth::attempt($credentials)){
            $request->session()->regenerate();

            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'user_code' => 'ユーザコードとパスワードが一致しません'
        ])->onlyInput('user_code');
    }
}
```

---

このメソッドは、節頭でご紹介した公式ドキュメントに忠実です。
- `$request`：リクエストデータ，フォームから送信されたデータという認識でOK
- `validate(array)`：array内の規則に基づいてバリデーションを行う
- `$credentials`：フォームデータにバリデーションを実施した結果
- `Auth::attempt($credentials)`：$credentialsに基づき認証を行う
- `session()->regenerate()`：セッションの再生成，**これは必須**
- `back()`：前のページへ戻らせる

#### `session()->regenerate()`
**この処理は必須です。面倒くさがらずに書いてください。訳が分からなくても書いてください。**

これにより、認証成功時にセッションを再生成します。**セッション固定攻撃の対策**となります。
これがどんな攻撃かということは本記事の範疇を超えるため説明しませんが、**これがないと明らかな脆弱性になります**。

https://www.ipa.go.jp/security/vuln/websecurity/session-management.html#:~:text=%E3%80%8C%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3ID%E3%81%AE%E5%9B%BA%E5%AE%9A%E5%8C%96%EF%BC%88Session%20Fixation%EF%BC%89%E3%80%8D

#### `validate()`
連想配列として記述された規則に則って、リクエストデータの検証を行います。
今回は、`user_code`と`password`がどちらも文字列として入力されるべきです。そのため、それぞれの要素をキー、規則を値として、`validate()`内の配列を記述します。
```php:validate()
$request->validate([
    'user_code' => ['required', 'string'],
    'password' => ['required', 'string']
]);
```

このバリデーションは、厳密にやっていけばもっと複雑になっていきます。`Validator`を作成するとか、そこからアロー関数を引っ張るとか…
ただ、ここでそんなに複雑にしても仕方ないです。Laravelでのフォームの扱いに慣れたとき、その知識を今回のようなログイン認証時に用いれば良いと思います。
気になる方は、バリデーションに関する公式ドキュメントをお読みください。

https://laravel.com/docs/12.x/validation

#### `Auth::attempt($credentials)`
`Auth`ファザードの`attempt`メソッドにより、`$credentials`の内容でユーザ認証を行います。**デフォルトでは`User`モデルに対して認証を行います。後述する「認証モデルの選択」を忘れずに行ってください**。

必要に応じて、`attempt(array)`を利用することも可能です。この場合、配列は以下の形式となります。
```php:attempt(array)
Auth::attempt([
    'column_name1' => $credentials['for_column'],
    'column_name2' => any_data_for_culumn,
    ...
]);
```
つまりは、**認証に使う属性（カラム）と、それに対応させるデータを選択・追加できる**ということです。筆者は使い方にあまり想像がつきませんが、「そういうこともできる」ということを知っておいてください。

https://laravel.com/docs/12.x/authentication#:~:text=Specifying%20Additional%20Conditions

#### `back()->withErrors(array)`
認証失敗時、`withErrors(array)`により、フラッシュ（セッション）にエラーメッセージを入れつつ、リクエスト送信元へリダイレクトさせます。`withErrors(array)`は、エラーメッセージを入れるだけなので、これがなくても処理に問題はありません。

ただ、エラーメッセージが表示できるような準備はしておいたほうが吉でしょう。いずれ「エラーメッセージも出せるようにする」としたときに、ちょっとだけ楽になります。

## 認証モデルの設定
**ユーザ認証に用いるデフォルトのモデルを設定します**。編集箇所をコメントアウトしつつ示していますので、間違えないように注意してください。
なお、**認証モデルをデフォルトにしない（→新たな認証モデルを追加する）場合は、後述する「認証モデルの追加」をお読みください**。
```php:config/auth.php
<?php
return [
    /* some writing... */
    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'password' => env('AUTH_PASSWORD_BROKER', 'members'), // 第２引数を'members'に変更
    ],

    /* some writing... */
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'members', // 'users'を'members'に変更
        ],
    ],

    /* some writing... */
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],

        // ここから追記
        'members' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\Member::class),
        ],
        // ここまで追記
    ],

    /* some writing */
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],

        // ここから追記
        'members' => [
            'provider' => 'members',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ]
        // ここまで追記
    ],

    ...
];
```

また、Membersモデルを認証機能に対応させます。
```php:app/Models/Member.php
use Illuminate\Foundation\Auth\User as Authenticatable // 追記

class Member extends Authenticatable // extends Modelからextends Authenticatableに変更
{
    ...
}
```

---

### `config/auth.php`の編集
ここでは、`config/auth.php`にある`defaults`部分の記述を変えることで、デフォルトの認証モデルを変更しました。
また、今回は「なるべく編集量を少なくする」ことを目的として、`guards`の`web`要素の記述を変更しています。この方法は、心の底からデフォルトの認証モデルを変えたい場合のみに限ったほうが良いと思われます。
この`guards`に要素を追加することで、認証モデルの追加が可能です。詳細は、後述する「認証モデルの追加」をお読みください。

### 自作モデルの認証対応化
自作したモデルを`Illuminate\Foundation\Auth\User`から拡張したものにすることで、ユーザ認証に対応させます。**これを忘れるとユーザ認証が行えません**。

## 認証モデルの追加
ここでは、**認証モデルの追加を行います**。**デフォルトの認証モデルを変更することとは違います**。

`config/auth.php`の`defaults`，`password_timeout`以外の要素に記述を追加します。
```php:config/auth.php
'guards' => [
    'web' => [...],
    'member' => [
        'driver' => 'session',
        'provider' => 'members',
    ],
],

'providers' => [
    'users' => [...],
    'members' => [
        'driver' => 'eloquent',
        'model' => env('AUTH_MODEL', App\Models\Member::class),
    ],
]

'passwords' => [
    'users' => [...],
    'members' => [
        'provider' => 'members',
        'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
        'expire' => 60,
        'throttle' => 60,
    ],
],

...
```

また、**追加した認証モデルで認証やその確認をする場合**は、以下のように行ってください。
```php
Auth::guard('members')->attempt(array); // Auth::attempt(array)を利用したいとき

Auth::guard('members')->login($model); // Auth::login($model)を利用したいとき

...
```

---

### 結局何が行われているの？
記述した`config/auth.php`の一部要素に関して補足します。

#### `providers`
ここで**認証を行うモデルがどれなのか**を指定します。
「`'users'`という認証要素に対しては、`eloquent`（→Laravelのモデルドライバ）にある`user`モデルを利用するよ」
「`'members'`という認証要素に対しては、`eloquent`にある`member`モデルを利用するよ」
といった要領で、**どのモデルを認証対象とするか指定する**ことができます。

#### `guards`
ここで**認証対象を確認する窓口**を作成します。
- 「`'web'`という窓口は、`'users'`という要素で認証されているか確認する」
- 「`'member'`という窓口は、`'members'`という要素で認証されているか確認する」

何のモデルでもログインしていなければ、もちろんですが、そもそもこれらを通過することはできません。
さらに、仮に`User`としてログインしていたとしても、`'member'`という`guard`は通過することができません。
仮に`Member`としてログインして初めて、`'member'`という`guard`を通過することができます。

このように、**「どういう認証を受けているか」を確認するもの**が`guards`です。

### `Auth::guard('guard')`
`Auth`ファザードのメソッドを利用したいときは、**`Auth::guard('guard')`を介してメソッドの呼び出しをする必要があります**。
引数となる`'guard'`には、`config/auth.php`の`guards`要素に追加したキーを指定してください。今回のように`'member'`を追記した場合は、`Auth::guard('member')`となります。

この辺りの設定が必要なことを、公式ドキュメントではしれっと書いています。

https://laravel.com/docs/12.x/authentication#:~:text=Accessing%20Specific%20Guard%20Instances

## 最後に
お疲れ様でした。長くなりましたが、これらがLaravelでのログイン認証の基本的な実装方法となります。

デフォルトで認証機能が備わっているからこそ、自作モデルでの認証機能実装はハードルが高くなっていると感じました。

Laravelでちょっと高度なアプリまで考えている方の助けになりましたら幸いです。