🕌

laravelの認証をカスタマイズできるようになる

2022/07/20に公開

laravelのガード認証について理解してカスタマイズする(1年目〜3年目向け)

今回の記事を読むとlaravelのガード認証について体系的に学び軽くカスタマイズできるようになります。
体系的に学ぶことでわからない箇所や調べるべき箇所が明確になり、実装にスムーズに着手できると思っています。

「laravelの認証の仕組みって何?」「忘れたからもう一回学び直したい」そんな方も対象になっています。

ではぼちぼちと行ってみましょう!

開発環境について

・laravel8.*
・mysql
・nginx

よく見るコマンド一発で作成されるログイン画面を用意しています。

まず最初にGuard(認証)について

語弊があるかもしれないですが、よくGuard認証というのを言葉にします。これは認証の種類にGuardというものがあるのではなく、「Guard=認証」という認識が正しいかもしれません。

ではlaravelのguardは何が必要なのでしょうか?
以下にlaravelのguardについて記載されています・

config\auth.php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
],

なるほど、、guardは配列でいくつかの認証方式を持つことができるのかということが何となく分かると思います。
そして一つの認証方式に対してdriverproviderというものがあるということが分かります。

簡単にdriverとproviderを説明します。
driver・・・ログインの認証情報の管理方法。sessionとはtokenとか。(デフォルトはweb)
provider・・・認証に使用するユーザーをどういった方法でログインされるかがまとまっているところ。(デフォルトはusers)

認証方法をカスタマイズする際はdriverproviderを修正、もしくは自作することが多いということだけ頭においておけば問題ないです。

認証までの流れ(laravelのどこを通ってるのか)

これを読んだほうがスムーズに理解できますが、時間がない人向けにちょろっと書いてみます。
https://reffect.co.jp/laravel/laravel-authentification-by-code-base

まずはcontrollerを呼び出すまでの流れです

  1. クライアント側でemailとpasswordを入力しログインボタンを押下
  2. guestミドルウェアの処理実行
  3. Auth\LoginControllerのloginメソッドを実行

ここからはloginメソッドの中身を見ていきましょう。(loginメソッドはAuthenticatesUsersトレイトに定義されています)

AuthenticatesUsers.php
public function login(Request $request)
{
    // ④
    $this->validateLogin($request);

    // ⑤
    if (method_exists($this, 'hasTooManyLoginAttempts') &&
        $this->hasTooManyLoginAttempts($request)) {
        $this->fireLockoutEvent($request);

        return $this->sendLockoutResponse($request);
    }

    // ⑥
    if ($this->attemptLogin($request)) {
        if ($request->hasSession()) {
            $request->session()->put('auth.password_confirmed_at', time());
        }

        return $this->sendLoginResponse($request);
    }

    // ⑦
    $this->incrementLoginAttempts($request);

    // ⑧
    return $this->sendFailedLoginResponse($request);
}
  1. リクエストされたemailとパスワードのバリデーション
  2. ログイン回数のチェック
  3. ログインの処理
  4. ログイン回数を増やす
  5. ログイン失敗時のレスポンス

認証をカスタマイズしてみよう!!

実はここからが本題です。色々理解しながらカスタマイズしてみましょう。
今回はguardはsessionで固定にしようと思います。

ではこんなカスタマイズがしたいと思ったときはないでしょうか。

  1. ログイン条件を変更したい
  2. マルチ認証を実装したい
    (今後追記していく予定です!)

では一つずつやっていきましょう!

1.ログイン条件を変更したい

laravelの標準のログイン条件はemailとpasswordになります。(remember_tokenは一旦置いときましょう)
今回は電話番号mobile_phone_numberをログイン条件として追加してみましょう。
事前準備として登録画面とログイン画面に電話番号のテキストボックスの追加とDBにmobile_phone_numberカラムを追加しておきます。

laravelの認証までの流れを簡単に理解できたと思いますので、大体どこを修正すればよいか分かると思います。修正箇所は以下になります。
上述した流れでいうとここらへんが怪しいですね。
4. リクエストされたemailとパスワードのバリデーション
6. ログインの処理

では修正していきましょう。
まずはloginメソッド内のvalidateLoginメソッドでログイン画面からのリクエストをバリデーションしていますので、ちゃちゃっとLoginControllerでオーバーライドしちゃいましょう。

Auth\LoginController
use Illuminate\Http\Request;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
 * Validate the user login request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return void
 *
 * @throws \Illuminate\Validation\ValidationException
 */
protected function validateLogin(Request $request)
{
    $request->validate([
        $this->username() => 'required|string',
        'password' => 'required|string',
        'mobile_phone_number' => 'required|string',
    ]);
}

いい感じにバリデーションできていますね。

では次は実際にログイン画面からのリクエストを使ってユーザーを認証してる箇所を覗いてみましょう。
認証で要となるのはloginメソッドのattemptLoginメソッドでしたよね。
さて。attemptLoginメソッドでは認証に成功したらtrue、失敗したらfalseを返すのですが実際の処理はどうなっているのでしょう。

AuthenticateUsers.php
protected function attemptLogin(Request $request)
{
    return $this->guard()->attempt(
        $this->credentials($request), $request->boolean('remember')
    );
}

attemptメソッドを呼んでいるだけですね。そしてattemptメソッドの引数に何やらリクエスト情報を渡しているっぽいです。(attemptメソッドはリクエストされた情報からユーザーを見つけて、認証するまでのロジックがまとめられたものです。今はこんなものがある程度の認識で大丈夫です)
credentialsの中身を見てみると、リクエスト情報からログイン画面のフォームで送られてきた情報を取得しているのが分かります。

AuthenticateUsers.php
protected function credentials(Request $request)
{
    return $request->only($this->username(), 'password');
}

じゃあここを修正すればいいじゃないか!ということです。
さっそくLoginControllerでオーバーライドしていきましょう。

LoginController.php
protected function credentials(Request $request)
{
    return $request->only($this->username(), 'password', 'monbile_phone_number');
}

これでログイン画面で電話番号だけを間違えてログインしようとするとログインできません。
正しい電話番号を入力するとログインできるようになったと思います。

要するにcredentialsメソッドで作った情報でusersテーブルから検索して一致していたら認証させるという感じですね。(すごい簡単に言うと)

ではこれで第一段階の「ログイン条件を変更する」は完了です!

2. マルチ認証を実装したい

1の実装ではデフォルトで用意してある、usersテーブルしか認証のテーブルとして使用できません。
基本的に案件ではusersテーブル以外使用することが多いかもしれません。テーブルを変えるだけならauth.phpのusersを該当のテーブル名に変えると上手くいきます。

では、マルチ認証をしたい時はどうなるのでしょう?

そんな場合にも対応ができるように、自分で用意したテーブルを認証用のテーブルとして扱うように修正してみましょう。

冒頭でprovidersというものを説明しましたが、今回はprovidersを作成していきます。
providerには誰(モデル)をどういったロジック(ドライバ)で認証するかをといった認証方法がまとめられています。

では、まず必要な作業を列挙してみます。

  1. 認証用のテーブルを作成する。(今回はadminsというテーブルを作成します)
  2. EloquentUserProividerクラスを継承した、プロバイダを作成する
  3. 2で作成したプロバイダをmy-adminsという名前で登録する
  4. config/auth.phpの修正をする

では一つづつ見ていきましょう

認証用のテーブルを作成する

今回は以下のカラム情報を持ったadminsテーブルを作成します。

認証情報には名前とパスワードが一致したらログインできる想定です。(本来名前で認証することは少ないと思いますが、分かりやすいと思ったので)

そしてadminsテーブルのモデルもここで用意しておきましょう。

Models\Admin.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class Admin extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];
}

Authenticatableクラスを継承していますね。
後述するEloquentUserProviderクラスではログインのロジックがまとめられています。各メソッドの戻り値がAuthenticatableになりますので、ここは必須です。

ではAuthenticatableメソッドとはなんぞや?と思った方もいると思いますので軽く説明します。
AuthenticatableクラスはIlluminate\Contracts\Auth\Authenticatableを実装したクラスになります。
Authenticatableインターフェイスには認証処理でユーザー情報へのアクセスに利用されるメソッドが定義されています。

まあそんなこんやで第一ステップは完了です!

EloquentUserPrpividerクラスを継承した、プロバイダを作成する

まずはapp/Http/AuthにAdminProvider.phpというファイルを作成しておきます。(ファイルの場所はProviderのは以下でもいいかもしれません。)

上述したとおり、EloquentUserProviderクラスには認証のロジックがまとめられています。
なぜEloquentUserProviderクラスを継承したクラスを作成するかというと、自分で作成したモデルに対して、どういったロジックで認証させるかの部分を定義しないといけないからです。

Auth\AdminProvider.php
<?php

namespace App\Http\Controllers\Auth;

use Illuminate\Auth\EloquentUserProvider;

class AdminProvider extends EloquentUserProvider
{
    /**
     * 与えられた credentials からユーザーのインスタンスを探す
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        return parent::retrieveByCredentials($credentials);
    }
}

retrieveByCredentialsだけ定義しましたが、これはなくても構いません。
adminsからユーザー情報を取得できるかのメソッドを自由にカスタマイズできます。
デフォルトでは単にnameとpasswordで検索しているだけです。

例えばですが、adminsに権限masterが紐付いているとして、権限masterではなければログインを失敗させる(nullを返す)とかですかね。

第二ステップも完了です!

2で作成したプロバイダをmy-adminという名前で登録する

2で作成したAdminProviderをmy-adminという名前でAuthに新しい認証ドライバとして認識してもらえるように登録しましょう。
サービスコンテナへのバインドと混同してしまいますが、Auth機能にバインドするというイメージです。

bootメソッドをこんな感じにします。

Providers\AuthServiceProvider
/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    \Auth::provider('my-admin', function ($app, array $config) {
        return new \App\Http\Controllers\Auth\AdminProvider($app['hash'], $config['model']);
    });
}

これで作成したAdminProviderがAuthでmy-adminという名前で使えるようになりました!

config/auth.phpの修正をする

ここまで来たら後はconfigを作成するだけです。

こんな感じにしてみましょう。

config\auth.php
<?php

return [
    // ①
    'defaults' => [
        'guard' => 'web',
        'passwords' => 'admins',
    ],

    // ②
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

    // ③
    'providers' => [
        'admin' => [
            'driver' => 'my-admin',
            'model' => App\Models\Admin::class,
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => 10800,
];

これでフロント側をnameとpasswordでログインできる画面を作成したら、もうログインできる状態です。
最後に、①②③について説明します。

実は③->②->①とボトムアップで説明したほうが分かりやすいです。
③のprovidersにadminという名前で今回作成したmy-adminを認証ドライバとして登録します。
登録したadminを②のガードのwebのプロバイダに設定します。
最後はデフォルトのガードを①でwebを使用するように定義します。

後は、ログイン画面を2つ用意して、それぞれで今回作成したガードを使い分ければマルチ認証の完成です!

終わり

お疲れさまでした!
認証関連は一度触らないと全くわからないですが、ちょっと分かるようになったのではないでしょうか?

ぼちぼち新しいカスタマイズ方法も追加していく予定です!

Discussion