🐺

【Laravel】既存テーブルで認証&認証方法を差し替えるTips

2020/09/19に公開

はじめに

Qiitaで書いていた自分自身の記事です。

既存システムの部分リニューアルにともなって
新設部分には Laravel をベースとして使用することとなり、
認証処理を既存のテーブルで行うようにしたときの tips です。

私が実際に利用した Laravel のバージョンは 5.7 になります。

対象とする読者

おことわり
・新規開発や、認証テーブルを変更・新設できる案件なら
 素直にベースのものを使用しよう。
・とりあえず認証させるという点においてのみ記載しています。
・この tips は複合主キーには対応していません…。ごめんよ(・ω・`)

目次

php artisan make:auth した後をベースとして、

認証に使用するテーブルを変更
認証に使用するモデルを変更
テーブルの主キーが "id" ではない場合
ID/PW のログインアカウントに使用するカラムの変更
ID/PW のパスワードに使用するカラムの変更
認証するロジック自体を変更

…を行う具体的な方法についての tips です。

他の書き方や方法も色々あると思いますが
これらの方法を組み合わせることで
単一主キーのテーブルはおおよそ対応できるかなと思います。

認証に使用するテーブルを変更

デフォルトでは \App\User クラスが認証用のユーザーテーブルのモデルとなっており、
これをそのまま使用する場合は、テーブル名の指定を増やすだけでも対応できます。

app/User.php

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

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

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'other_table'; // Laravel の命名規則に従っていないテーブルでも、明示的に指定すればOK
}

認証に使用するモデルを変更

または、\App\User は一応 users テーブルのモデルなので、
これを参考に新たにモデルを定義して利用することもできます。

\App\User\Illuminate\Foundation\Auth\User クラスを継承しており
(このクラスが認証に必要な機能を持つトレイトなどを使用しています)、
同じように継承します。

app/Path/To/Models/Staff.php

namespace App\Path\To\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Staff extends Authenticatable
{
    use Notifiable;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'staff'; // Laravelの命名規則に従っていないテーブルでも、明示的に指定すればOK
}

そして、これを使用するように指定します。

なお、Laravel の認証機能はまだイマイチ理解が追いついていませんが
ドキュメントなどからざっくり表現しますと、
「ガード」という認証の種類ごとに「プロバイダ」が指定されており、
「プロバイダ」ではその処理の実装(driver)と対象のデータ(model)を指定している…
というような感じのイメージで良いのかなと思います…
(理解が浅いためここでは深追いしません。あしからず。)

作ったモデルを認証で使用するように指定

config/auth.php

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            //'model' => App\User::class, // ここでモデルを指定しているので、
            'model' => App\Path\To\Models\Staff::class, // 用意したモデルを使用するように変える
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

テーブルの変更、モデルの変更はこのようになります。

テーブルの主キーが"id"ではない場合

次に、Laravel では初期値としてテーブルの主キーが「id」という前提になっています。
そうではない場合は、テーブル名と同様に指定してあげればOKです。

仮に「primary_key」というカラム名だとしたら以下のような感じ。
(例では先程追加したモデルを流用しています。)

app/Path/To/Models/Staff.php

namespace App\Path\To\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Staff extends Authenticatable
{
    use Notifiable;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'staff';

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'primary_key'; // これを追記
}

ちなみに余談ですが、
(認証を通すだけなら必要なかったかと記憶しておりますが、)
文字列型だったり連番でなかったりでうまくいかない場合は…
以下のプロパティも要チェックや!

app/Path/To/Models/Staff.php

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false; // デフォルトでは自動インクリメントが true になってるよ

ログインアカウントに使用するカラムの変更

ログインIDのカラムの指定周りは
\Illuminate\Foundation\Auth\AuthenticatesUsers というトレイトとして実装されており、
username というメソッドで一元管理されているような形になっています。

make:auth 後では \App\Http\Controllers\Auth\LoginController
このトレイトを利用しているため、username メソッドをオーバーライドします。

また、後述していますがリクエストのキーにも密接に関連しているようで(たぶん)、
ログインIDの部分に関しては view も変える必要がありました。

# 実際には view 変えなくても行けるんじゃないかと思うんだけど…。
 とりあえずは現時点で私が実際にできた方法について言及しています。
 ご存知の方はこっそりご教示くだしあorz

デフォルトでは「email」になっています。

LoginControllerusername というメソッドを追記、
return する文字列を変更したいカラム名にします。

app\Http\Controllers\Auth\LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**
     * Illuminate\Foundation\Auth\AuthenticatesUsers
     * 
     * Get the login username to be used by the controller.
     *
     * @return string
     */
    public function username() // このメソッドを追記
    {
        return 'login_account'; // 対象のカラム名に。後述するように view も変えます
    }
}

そしてviewの resources\views\auth\login.blade.php
「email」を対象のカラム名(上記の例では「login_account」)に合わせて変更します。

パスワードに使用するカラムの変更

パスワードのカラムの指定は
Illuminate\Auth\Authenticatable というトレイトとして実装されており、
App\User などの認証用のユーザーテーブルのモデルが
(親クラスの Illuminate\Foundation\Auth\User が)
利用しています。

メソッドを経由してモデルが自身のパスワードの値を返すという形になっており、
デフォルトでは「password」になっています。

こちらは以下の1箇所のみ追記してオーバーライドすればOKです。

app/path/to/model/staff.php

namespace App\Path\To\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Staff extends Authenticatable
{
    use Notifiable;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'staff';

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'primary_key';

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword() // これを追記
    {
        return $this->hash; // 対象のカラム名に
    }
}

認証するロジック自体を変更

(前置き)ドライバーの登録について

認証のロジックは users プロバイダとして指定されている
eloquent というドライバーが実装を持っており、
最終的にはこれを真似て custom というドライバーを別途作成し、差し替えます。

# ドライバーの登録は、クラス内コメントによると(最後に詳細は記載しますが)
 \App\Providers\AuthServiceProvider クラスに書くことで行うことができます。

eloquentというドライバーは、
追っていくと元の処理は \Illuminate\Auth\EloquentUserProvider が持っています。

ログイン処理の対象になるメソッドは validateCredentials というメソッドで、
true を返せば認証OK!という感じになっており、
この内容を変えることが認証ロジックの変更、ということになります。

クラスを自前のものに差し替えてメソッドをオーバーライドするという方法もありますが、
今回はさらに奥、パスワードのハッシュ化に使用している
Hasher クラスを差し替えることで認証処理を変更します。

認証ロジックの詳細説明(処理追いたい人向けの話。読み飛ばして大丈夫)

先程の \Illuminate\Auth\EloquentUserProvider
validateCredentials メソッドからさらに処理を追っていくと…

=====

・その実際の処理は $this->hasher が持っている

$this->hasher はコンストラクタで渡されている

・それが CreatesUserProviders などで渡される $this->app['hash'] であり、
 その中身は Illuminate\Hashing\HashManager

HashManager の各メソッド内では $this->driver()->○○ という形で処理を実行しており、
 HashManager は各ハッシュ化方式ごとの Hasher クラス(ドライバー)を保持していて
 $this->driver() で一つの Hasher を返している
 # 最終的なハッシュ化はこの Hasher クラスが担っているが、
  実行は HashManager 経由ということ。

Hasher クラスを返す $this->driver() は引数なしで実行されており、
 その場合、対象を示すキー文字列は $this->getDefaultDriver() で決まる

$this->getDefaultDriver()
 return $this->app['config']['hashing.driver'] ?? 'bcrypt';

=====

…ということなどがわかります。ゼェゼェ…

で、結局どうするの?

・自前の Hasher クラスを作成
HashManager$this->getDefaultDriver() で、
 作成した Hasher クラスを指定するための任意のキーが返るようにしておく
HashManagerに、キーと作成した Hasher クラスを登録

ことで、意図した認証処理に差し替えることができます。

自前の Hasher クラスを作成

Illuminate\Hashing\BcryptHasher などを参考に、Hasher クラスを作成します。
make() でハッシュを返す、check() でパスワードとハッシュを比較する。というのがポイントです。

今回は MySQL の PASSWORD 関数でハッシュを求めるようなクラスを作成しました。

app/Path/To/Hashing/MySqlPasswordHasher.php

<?php

namespace App\Path\To\Hashing;

use RuntimeException;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Hashing\AbstractHasher;

class MySqlPasswordHasher extends AbstractHasher implements HasherContract
{
    /**
     * Create a new hasher instance.
     *
     * @param  array  $options
     * @return void
     */
    public function __construct(array $options = [])
    {
    }

    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     *
     * @throws \RuntimeException
     */
    public function make($value, array $options = [])
    {
        $row = \DB::selectOne("SELECT PASSWORD(?) AS hash", [ $value ]);

        if (empty($row->hash))
        {
            throw new RuntimeException('MySqlPassword hashing not supported.');
        }

        return $row->hash;
    }

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array  $options
     * @return bool
     *
     * @throws \RuntimeException
     */
    public function check($value, $hashedValue, array $options = [])
    {
        return $this->make($value) === $hashedValue;
    }

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = [])
    {
    	return strpos($hashedValue, "*") === 0 && strlen($hashedValue) == 41;
    }
}

HashManager$this->getDefaultDriver() で、作成した Hasher クラスを指定するための任意のキーが返るようにしておく

Laravel では基本的にはアプリケーション内で1つのハッシュ化しか使用しない形になっており、
デフォルトでは bcrypt になっています。

他のハッシュ化を使用しないのであれば、
$this->app['config']['hashing.driver']、つまり以下を変えればOKになります。

config/hashing.php

    //'driver' => 'bcrypt',
    'driver' => 'mysql_password', // アプリ内固定の場合は設定変更で。

もし、アプリケーション内で複数のハッシュ化方式を使用したいという場合は
自前の HashManager クラスを作成し、
$this->getDefaultDriver() をオーバーライドしてそれを使えばOKです。

今回はこちらの方法で進めます。

app/Path/To/Hashing/CustomHashManager.php

<?php
namespace App\Path\To\Hashing;

use Illuminate\Hashing\HashManager;

class CustomHashManager extends HashManager
{
    /**
     * Get the default driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return 'mysql_password'; // 設定を変更せず対応するならクラスで対応
    }
}

コンストラクタやセッターなどで値を変えられるようにしておくというのもアリですね。

HashManager に、キーと作成した Hasher クラスを登録

これには二通りの方法があります。

customドライバーを登録する際、EloquentUserProviderHashManager を渡すときに、
 $(HashManager)->extend() を使って登録する
 # $(HashManager)->customCreators[$driver] に登録されます

・または、HashManager クラスにあらかじめ
 'create' . Str::studly($driver) . 'Driver' というメソッドを作成する
 ※ Str::studly() はパスカルケース変換処理です。
   キーが 'mysql_password' だと 'MysqlPassword' になるので、
   function createMysqlPasswordDriver() というメソッド名で作成することになります。

今回は前者の方法で実装しますので、
次の「ドライバーを登録する」項で一緒に記載します。

# 具体的にどうなっているのかは
 php:vendor\laravel\framework\src\Illuminate\Support\Manager.php
 を参照ください

# 後者の場合はオリジナルを汚さず作る場合、新たに独自クラスを定義して拡張することになるのと、
 Laravel 内の MySQL 系メソッド等の表記は "MySql" となっていて
 Str::studly() で変換できるキーとなると my_sql_password、 mySql_password、MySqlPassword
 などになるのがちょっと嫌だった

(前項続き+)ドライバーを登録する

これが最後になります。

下記では、\Illuminate\Auth\EloquentUserProvider クラスを
新たに custom というドライバーとして登録しています。
(独自クラスを使用することも、もちろん可能です。)

このタイミングで、前項の
HashManager に、キーと作成した Hasher クラスを登録」し、
$app['hash'] の代わりに任意の HashManagerHasher クラスに差し替えていきます。

app\Providers\AuthServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

// 以降のuse文を追記
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Application;
use Illuminate\Auth\EloquentUserProvider;

use App\Path\To\Hashing\CustomHashManager;
use App\Path\To\Hashing\MySqlPasswordHasher;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

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

        // ここが追記部分
        // 'custom' ドライバーを登録
        Auth::provider('custom', function (Application $app, array $config) {

            // デフォルトの $app['hash'] の代わりに、自前の HashManager を準備
            // この HashManager のデフォルトの Hasher 指定は 'mysql_password' になっている。
            //( config を使用する場合はデフォルトの HashManager でOKな部分。)
            $customHashManager = new CustomHashManager($app); 

            // extend() を使用して、'mysql_password' というキーで自前の Hasher を登録
            $customHashManager->extend('mysql_password', function ($app) { return new MySqlPasswordHasher; } );

            // コンストラクタで HashManager を渡す。
            // ( EloquentUserProvider クラスを差し替えたい場合はこのタイミングで変えられます)
            return new EloquentUserProvider($customHashManager, $config['model']);
        });
    }
}

最後に、認証用のユーザーモデルを指定したときと同じような感じで、
config/auth.phpcustom ドライバーを指定します。

config/auth.php

    'providers' => [
        'users' => [
            //'driver' => 'eloquent', // 元
            'driver' => 'custom', // 追記
            //'model' => App\User::class,
            'model' => App\Path\To\Models\Staff::class,
        ],

これで認証するロジックも変更することができました。

締め

奥が深いので、まだまだ他の方法や
記載した内容のみでは対応できないケースなどもあるかと思いますが
ひとまずは以上となります。
(まだまだわからないことだらけ…)

それでは、おつかれさまでした!

Discussion