LaravelはPHPのフレームワークであり、よくwebサーバー、PHP、データベースといった構成でwebサイトを構築します。この構成でしばしば全く異なる2つの保存形式を持つユーザーらをそのまま同じようにログインさせたい、という要望が出てきます。例えば、管理者ユーザーと通常のアカウントユーザーを同じログイン画面で認証したい場合です。この記事では、Laravelで一つの認証ガードで複数のモデルを扱う方法を紹介します。
Laravelで複数の認証モデルを扱うために次のようなカスタムUserProviderを作ることができます。このUserProviderを利用することで、異なるモデルのユーザーを同時に認証することができます。
<?php
namespace App\Library\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class MultiUserProvider implements UserProvider
{
/**
* 複数のUserProviderをまとめて登録する静的メソッド
*
* @param string $name プロバイダーの名前
* @param array<int, UserProvider|string> $providers まとめるプロバイダー
* @return void
*/
public static function register(string $name, array $providers): void
{
// 各プロバイダーがUserProviderインスタンスでない場合は名前を元に作成する
$providers = array_map(function ($p) {
if ($p instanceof UserProvider) {
return $p;
} else {
return \Auth::createUserProvider($p);
}
}, $providers);
// 認証システムにカスタムプロバイダーを登録する
\Auth::provider($name, function () use ($providers) {
return new MultiUserProvider($providers);
});
}
/**
* コンストラクタ
*
* @param UserProvider[] $providers 複数のUserProviderインスタンス
*/
public function __construct(protected array $providers)
{
}
/**
* IDでユーザーを取得する
*
* @param mixed $identifier ユーザーの識別子
* @return Authenticatable|null
*/
public function retrieveById($identifier)
{
// 各プロバイダーからユーザーを検索
foreach ($this->providers as $provider) {
if ($user = $provider->retrieveById($identifier)) {
return $user;
}
}
return null;
}
/**
* トークンでユーザーを取得する
*
* @param mixed $identifier ユーザーの識別子
* @param mixed $token トークン
* @return Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// 各プロバイダーからトークンを使ってユーザーを検索
foreach ($this->providers as $provider) {
if ($user = $provider->retrieveByToken($identifier, $token)) {
return $user;
}
}
return null;
}
/**
* リメンバートークンを更新する
*
* @param Authenticatable $user ユーザーオブジェクト
* @param string $token リメンバートークン
* @return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// 各プロバイダーに対してリメンバートークンを更新
foreach ($this->providers as $provider) {
$provider->updateRememberToken($user, $token);
}
}
/**
* 資格情報でユーザーを取得する
*
* @param array $credentials 認証資格情報(例: メールアドレスとパスワード)
* @return Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// 各プロバイダーから資格情報を使ってユーザーを検索
foreach ($this->providers as $provider) {
if ($user = $provider->retrieveByCredentials($credentials)) {
return $user;
}
}
return null;
}
/**
* 資格情報を検証する
*
* @param Authenticatable $user ユーザーオブジェクト
* @param array $credentials 認証資格情報(例: メールアドレスとパスワード)
* @return bool 認証が成功したかどうか
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
// 各プロバイダーで資格情報を検証
foreach ($this->providers as $provider) {
if ($provider->validateCredentials($user, $credentials)) {
return true;
}
}
return false;
}
/**
* ユーザーのパスワードを必要に応じて再ハッシュし、更新します。
* @param Authenticatable $user
* @param array $credentials
* @param bool $force
* @return void
*/
public function rehashPasswordIfRequired(
Authenticatable $user,
#[\SensitiveParameter] array $credentials,
bool $force = false
): void {
foreach ($this->providers as $provider) {
$provider->rehashPasswordIfRequired($user, $credentials, $force);
}
}
}
適当な場所に上記コードを置き、次にのように認証プロバイダを統合するようにLaravelに登録します。この処理はServiceProvider内で行います。
<?php
namespace App\Providers;
use App\UseCase\AccountBrowserAPI\Auth\MultiUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* 認証や認可のサービスを登録する
*
* @return void
*/
public function boot(): void
{
$this->registerPolicies();
// カスタムUserProviderを登録
MultiUserProvider::register(
'multi_accounts',
['admins', 'companies']
);
}
}
MultiUserProvider::register()メソッドを使って、複数のプロバイダーを一つのカスタムプロバイダーmulti_accountsに結合してLaravelに登録します。
最後にconfig/auth.phpファイルで定義したプロバイダーを使用することと認証ガードを設定します。
'guards' => [
'web' => [
'driver' => 'session',
// ここで結合プロバイダーを指定
'provider' => 'multi_accounts',
],
],
'providers' => [
// 結合対象プロバイダーの設定
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Eloquents\Account\Admin::class,
],
'accounts' => [
'driver' => 'eloquent',
'model' => App\Models\Eloquents\Account\Account::class,
],
// 結合してできたドライバーを使うプロバイダー
'multi_accounts' => [
'driver' => 'multi_accounts',
],
],
この設定では、multi_accountsプロバイダーをカスタムドライバーとして定義しています。このプロバイダーにより、AdminモデルとAccountモデルの両方を単一のプロバイダーとして使用することが可能です。こうすれば、AdminモデルとAccountモデルの両方を同じ認証ガードで扱うことができます。
余談として複数モデルを一つの認証ガードで扱う際に気にすべき点がいくつかあります。例えばログインIDの重複です。別々の場所に保存されているデータですが、ログインIDに相当する値は重複してはいけません。ログインを試みる時に予期せぬアカウントにログインを試みることになりがちです。また、ログインユーザーインターフェースを作った方が無難です。別々のモデルの全てを把握するのは手間ですし、元々のモデルとしての振る舞いの他にログインユーザーとしての振る舞いが求められる時があり、そういった時にモデルの違いを意識せずに済むのはプログラミングが楽になります。
追記: retrieveByIdでキーとなるIdが被ってもダメです。オートインクリメントを主キーにしていると踏みやすい問題です。ULID等を主キーにすると回避できます。