前置き
Laravelの認証ではIlluminate\Contracts\Auth\Guard
を実装することで、自前の認証ロジックを組み込むことができます。
https://readouble.com/laravel/8.x/ja/authentication.html#adding-custom-guards
例えば、このようなGuardを作成すると、パラメータにid
というキーの値が含まれている時、そのIDのUserとして認証をさせることができます。
この認証ロジックを工夫することで、JWTを使った認証や、その他外部サービスと連携した認証が行えるという訳です。。
MyGuard implements Guard
{
use GuardHelpers;
private Request $request;
private UserProvider $userProvider;
/**
* MyGuard constructor.
* @param Request $request
* @param UserProvider $userProvider
*/
public function __construct(Request $request, UserProvider $userProvider)
{
$this->request = $request;
$this->userProvider = $userProvider;
}
public function user()
{
$id = $this->request->get('id');
if ($id) {
$this->user = $this->userProvider->retrieveByCredentials(['id' => $id]);
}
return $this->user;
}
public function validate(array $credentials = []): bool
{
return false;
}
この自前のGuardをサービスプロバイダに登録します。
class AppServiceProvider extends ServiceProvider
{
...
public function boot()
{
Auth::extend('custom', function ($app, $name, array $config) {
return new MyGuard(request(), Auth::createUserProvider($config['provider']));
});
}
}
そして、auth.php
を編集し登録したサービスをアプリケーションで使用するように設定します。
'guards' => [
'web' => [
'driver' => 'custom',
'provider' => 'users',
],
最後に、web.php
に以下のようなルートを追加し、http://localhost/users?id=1にアクセスすると、DBにID1のユーザーが登録されている時、そのユーザーのnameが表示されます。
Route::get('/users', function () {
/** @var User $user */
$user = Auth::user();
return $user->name ?? 'not logged in';
});
本題
Laravelの認証に自前のロジックを組み込む方法はわかりました。しかし、上で作成したGuardはステートレスな物になっており、認証をするためには毎回id=1
のパラメータを付与する必要があります。
一度id=1
で認証したら、二度目以降はパラメータなしでも認証できるようにするにはどうしたら良いのでしょうか?
LaravelのドキュメントにはAuth::login($user, $rememberMe)
というメソッドを実行し、$rememberMe
をtrue
にすることで次回から自動ログインできる、と書いてあります。
https://readouble.com/laravel/8.x/ja/authentication.html#authenticate-a-user-instance
なので、以下のようにユーザーを取得したタイミングで、このメソッドを呼び出せば良さそうです。
public function user()
{
$id = $this->request->get('id');
if ($id) {
$this->user = $this->userProvider->retrieveByCredentials(['id' => $id]);
Auth::login($this->user, true)
}
return $this->user;
}
が、実はこれでは動きません。このAuth
ファサードのlogin()
メソッドは実はアプリケーションで使用されているGuard(今回はCustomGuard
)のlogin()
メソッドのエイリアスになっているので、CustomGuard
自体にlogin()
メソッドを実装する必要があるのです。
もう少し正確な話をすると「ステートフルな認証を行う場合、アプリケーションで使用するGuardはIlluminate\Contracts\Auth\StatefulGuard
を実装する必要があり、login()
メソッドはその中で実装しなければいけないメソッドの一つ」ということです。
StatefulGuard
はlogin()
メソッドの他にこれらのメソッドを実装する必要があります。
attempt()
once()
login()
loginUsingId()
onceUsingId()
viaRemember()
logout()
これらのメソッドを生真面目に実装しても良いのですが、認証状態を継続したい、という要件だけならSessionGuard
を継承してしまうのが早いです。
まず親クラスのSessionGuardのuser()
を呼び出し、ステートフルな認証情報からユーザーが取得できたら、それを返します。見つからなかった場合は、自前の認証ロジックを呼び出し、SessionGuard
のlogin()
メソッド(Auth::login()
と同じ意味)を呼び出し、ログイン状態を記憶します。
class MyGuard extends SessionGuard
{
public function __construct($name, UserProvider $provider, Session $session, Request $request = null)
{
parent::__construct($name, $provider, $session, $request);
}
public function user()
{
$this->user = parent::user();
if ($this->user) {
return $this->user;
}
$id = $this->request->get('id');
if ($id) {
$this->user = $this->provider->retrieveByCredentials(['id' => $id]);
$this->login($this->user);
}
return $this->user;
}
}
プロバイダーへの登録はこのようになります。
class AppServiceProvider extends ServiceProvider
{
...
public function boot()
{
Auth::extend('custom', function ($app, $name, array $config) {
$session = $this->app->make(Session::class);
return new MyGuard($name, Auth::createUserProvider($config['provider']), $session, request());
});
}
}
以上!