【PHP】【Laravel】カスタマイズ性の高いログイン機能の実装

システムを作るうえで多くの際に実装するログイン機能はLaravelのデフォルト設定でやる場合簡単ですが、カスタマイズする方法がすこし特殊になるためまとめてみました。
目次
はじめに
こんにちは。
クラウドソリューショングループのshimizu.shoです。
業務では主にwebアプリ開発を行っています。
ログインは、多くのシステムにおいて使用される機能の一つです。
PHP フレームワークである Laravel を使用すると簡単にその機能を実装することができますが、ログイン方法にカスタマイズ性を持たせたい場合は一工夫が必要です。今回は、その方法の1つをご紹介します。
ログインの仕組み
Laravelのログインではガードという仕組みが使われています。
管理者用のガード、一般のガードといったように権限の種類によって設定されるものです。
このガードの中に認証方法や認証状態の管理の方法、暗号化の仕方などが含まれています。

初期状態では認証方法はDB、認証の状態の管理はセッションを用いた認証が使われています。
Laravelの仕組みの中で認証を行ってくれるため、開発側では認証のファサードの関数を指定するだけで認証の実装ができます。
次章にて実際のコードを見せながら初期状態の動き、カスタマイズする際の実装の仕方を行っていきます。
実装例
今回の環境
PHP:8.2.12 Laravel: 10.48.20 Mysql: 10.4.32
Composerを使ってprojectを作ります
 $ composer create-project "laravel/laravel=10.48.20" プロジェクト名
プロジェクトを作った最初の段階では認証機能がないためlaravel/uiを導入して進めていきます。
 $ composer require laravel/ui
 $ php artisan ui:auth
この段階でloginに必要な実装は作られます。
認証周りの設定はconfig/auth.phpに書かれています。初期ではデータベースを利用して認証し、認証が通ったデータはセッションで保存されています。
現状ではデータベースが用意されていないのでデータベースを作っていきます。
[auth.php]
<?php
return [
  'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],
  'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],
    ],
]
データベースの作成に必要なmigrationファイルとseederファイルが必要です。
migrationファイルはデフォルトで作られているので実行するのみです。
 $ php artisan migrate
seederのファイルを作っていきます。
デフォルトのDatabaseSeeder.phpを書き換えていきます。
[DatabaseSeeder.php]
<?php
・・・・
public function run(): void
    {
        \App\Models\User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => Hash::make('test'),
        ]);
    }
}
seederも実行していきます。
 $ php artisan db:seeder
これでデータもそろったので、
ブラウザ上からログイン画面に登録したemailとパスワードを入力してログインを試してみます。

無事にログイン後の画面に入れました!

ここからが本題です。
認証方法を変更していきます。
今回は簡単に、リクエストにIDを追加して、パスワード一致+追加したIDが1の場合のみログインに成功するようにします。
まずはauth.phpに新しい認証設定を入れていきます。(driverは自作になるので命名しています)
[config/auth.php]
<?php
return [
  ・・・
  'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'custom' => [  //追加
            'driver' => 'session', //追加
            'provider' => 'custom', //追加
        ],
    ],
   'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'custom' => [ //追加
            'driver' => 'custom_provider', //追加(自由に命名)
            'model' => App\Models\User::class, //追加
        ],
    ],
]
custom_providerの中身を作る前に画面側のviewを作ります。
login.bladeの中身をコピーして以下の部分だけ追加しています。
[resources\views\auth\customLogin.blade.php]
<?php
・・・
<div class="row mb-3">
  <label for="password" class="col-md-4 col-form-label text-md-end">{{ __('パスワード') }}
    <div class="col-md-6">
        @error('password')
        <span class="invalid-feedback" role="alert">
            <strong>{{ $message }}
        </span>
        @enderror
    </div>
</div>
<div class="row mb-3"> //追加
    <label for="extra_id" class="col-md-4 col-form-label text-md-end">{{ __('外部ID') }} //追加
    <div class="col-md-6"> //追加
        @error('extra_id') //追加
        <span class="invalid-feedback" role="alert"> //追加
            <strong>{{ $message }} //追加
        </span>
        @enderror
    </div>
</div>                         
・・・
routesの中身、controllerの中身も作っていきます。
[routes/web.php]
<?php
・・・
Route::get('/customLogin', [App\Http\Controllers\CustomLoginController::class, 'showLoginForm'])->name('customLogin');
Route::post('/customLogin', [App\Http\Controllers\CustomLoginController::class, 'login']);
・・・
Route::middleware(['auth:custom'])->group(function () {
    Route::get('/customHome', [App\Http\Controllers\CustomHomeController::class, 'index'])->name('customHome');
});
・・・
[test/app/Http/Controllers/CustomLoginController.php]
<?php
class CustomLoginController extends Controller
{
    use AuthenticatesUsers;
    //ログイン画面の表示
    public function showLoginForm()
    {
        return view('auth.customLogin');
    }
  //ガードの指定
    protected function guard()
    {
        return Auth::guard('custom');
    }
    protected $redirectTo = '/customHome';
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
        $this->middleware('auth')->only('logout');
    }
}
[test/app/Http/Controllers/CustomHomeController.php]
<?php
class CustomHomeController extends Controller
{
    public function index()
    {
        return view('customHome');
    }
custom_providerの中身を作っていきます。
[app/Http/Controllers/CustomLoginController.php]
<?php
class AuthServiceProvider extends ServiceProvider
{
   ・・・
    public function boot(): void
    {
        Auth::provider('custom_provider', fn($app, array $config) => new CustomUserProvider($app['hash'], $config['model']));
    }
}
新しくCustomUserProviderのファイルを作成していきます。
- retrieveByCredentialsは取得する条件が記載されているので、リクエストからpasswordとextra_idを除いた
 メールアドレスのみをwhere条件に含んで1件のデータを取得しています。
- validateCredentialsでは「パスワードが入力とretrieveByCredentialsで取得したデータと一致しているか」「入力したextra_idが1」であるかをチェックしています。
[app/Providers/CustomUserProvider.php]
<?php
class CustomUserProvider extends EloquentUserProvider implements UserProvider
{
    /**
     * retrieveByCredentials をオーバーライド。
     * 認証対象のeloquentモデルを取得する。
     * 標準の場合設定しているモデルからデータを取得してくる。
     */
    public function retrieveByCredentials(array $credentials)
    {
        $credentials = array_filter(
            $credentials,
            fn($key) => ! (str_contains($key, 'password') || str_contains($key, 'extra_id')),
            ARRAY_FILTER_USE_KEY
        );
        if (empty($credentials)) {
            return;
        }
        $query = $this->newModelQuery();
        foreach ($credentials as $key => $value) {
            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } elseif ($value instanceof Closure) {
                $value($query);
            } else {
                $query->where($key, $value);
            }
        }
        return $query->first();
    }
    /**
     * validateCredentials をオーバーライド。
     * 取得したデータをどうバリデーションするかを定義する。
     *
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        if (is_null($plain = $credentials['password'])) {
            return false;
        }
        return $this->hasher->check($plain, $user->getAuthPassword()) && $credentials['extra_id'] === "1";
    }
}
これで必要なファイルの作成は完了です。
カスタマイズした画面でのログインを実施していきます。

ログイン画面でログインを実施します。

無事にログインできました。
ログインが失敗するケースも試してみます。
extra_idを2にして、メールとパスワードはデータベースに登録したデータを入力します。

ログインを試した結果↓

無事にバリデーションエラーで弾かれました。
しっかりとextra_idが認証条件に含まれています。
おわりに
今回はLaravelでのログインのカスタマイズ実装をまとめてみました。
カスタマイズして実装することでいろいろな認証パターンでの実装が可能になります。
みなさんのよりよいエンジニアライフを!
 
	             
  







