何がしたい?
ユーザーの作成・ログイン・パスワードリマインダーなど、ユーザー認証周りの機能を「任せとけ!」とワンタッチでサラッと作ってくれる男前でカッコいいLaravelさんですが、チョット待って、認証条件がEmailとパスワードだけじゃなくて、有効無効フラグや「ログイン権限」もチェックしたいのですが…。
という、Google先生ならサラッと教えてくれそうなテーマですが、サラッと教えてくれる答えがあまりぴったり来る感じではなかったので、僭越ながら穴埋め係させていただきます。
結論
1.ユーザーを探す条件を追加する
最も手軽な方法。
Userモデルに is_valid というカラムを追加しているようなケースに適用できます。
やることは、標準で app ディレクトリ内に作ってくれているログインコントローラにメソッドを追加するだけ。
// 冒頭にコレを追加するのを忘れずに
use Illuminate\Http\Request;
class LoginController extends Controller
{
// ...
/**
* ユーザーを探す条件を指定する
*
* @param \Illuminate\Http\Request $request
* @return Response
*/
protected function credentials(Request $request)
{
return array_merge(
$request->only($this->username(), 'password'), // 標準の条件
[ 'is_valid' => true ] // 追加条件
);
}
}
2.ユーザーを探す方法をカスタマイズする
1の方法だと、App\User のDBカラムしか検索対象になりません。
「ログイン権限」のような権限は別途RoleやPolicy的なモデルやLaravelの機能を使うことが多いと思いますが、そうした「別テーブル」や「別機能」で制限したい場合は、「ユーザーを探す方法」をカスタマイズします。
参考:https://laravel.com/docs/6.x/authentication#adding-custom-user-providers
#( ゚Д゚) passwordをnullにしとけばいいんじゃね?
(∩゚д゚) アーアーきこえなーい
ユーザープロバイダーを作成
場所はどこでも大丈夫なのですが、今回は標準のログインコントローラの横に置きました。
#わかりやすいけど、役割的にはHTTPと関係ないので、できれば Provider 配下(直下だとServiceProviderと区別しにくいので /Provider/UserProvider/MyUserProvider.php とディレクトリ切ったほうが良いかも)がオススメ。
namespace App\Http\Controllers\MyUserProvider;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Gate;
class MyUserProvider extends EloquentUserProvider implements UserProvider
{
/**
* 与えられた credentials からユーザーのインスタンスを探す
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
$user = parent::retrieveByCredentials($credentials);
// リレーション先の権限テーブルで can_login が true を持つユーザーに制限
if( $user->privileges()->where('privilege','can_login')->where('status',true)->exists() )
{
return $user;
} else {
return null;
}
}
}
Gateで権限管理をしている場合はこのように。
(Gateの使い方は別書に譲ります…)
public function retrieveByCredentials(array $credentials)
{
$user = parent::retrieveByCredentials($credentials);
if( Gate::forUser($user)->allows('feature.login') )
{
return $user;
} else {
return null;
}
}
もちろん、検索をゼロからカスタムすることもできます。
public function retrieveByCredentials(array $credentials)
{
$query = $this->createModel()->newQuery();
$user = $query
->where('login_id', $credentials['login_id'] ?? null )
// 検索条件を追加する
->join( 'privilege', .... )
->first();
return $user;
}
よくある「ユーザー名またはEMAILで…」とするならこんな感じ
public function retrieveByCredentials(array $credentials)
{
$query = $this->createModel()->newQuery();
$user = $query
->where('user_name', $credentials['login_id'] ?? null )
->orWhere('email', $credentials['login_id'] ?? null )
->first();
return $user;
}
AppServiceProviderで登録
ヒトテマですが、作成したユーザープロバイダは登録しないと使えません…。
今回はお手軽に AppServiceProvider で登録します。
public function register()
{
\Auth::provider('my_user_provider', function ($app, array $config) {
return new \App\Http\Controllers\Auth\MyUserProvider( $app['hash'], $config['model'] );
});
// ...
}
# Auth機能にUserProviderとして登録しています。
AppにServiceProviderを登録したり、
サービスコンテナにサービスを登録するのとは別機能なので混同しないように注意…。
configでユーザープロバイダを指定
最後に、configで認証ドライバを置き換えます。
'providers' => [
'users' => [
// 'driver' => 'eloquent', // 消して
'driver' => 'my_user_provider', // 追加
'model' => App\User::class,
],
以上。
3.認証方法をカスタマイズする(非推奨)
2の方法など生ぬるいわ……。
そのユーザーがログインできるかどうかのチェックをカスタマイズしたいのだ……👿。
という邪悪な思想をお持ちの方は、同じMyUserProviderにコレを追加sしてください。
public function validateCredentials(Authenticatable $user, array $credentials)
{
// 独自の認証ロジックをコール
if ( MySuperAuthAlgorithm::validateUser( $user->login_id, $user->password ) {
return true;
}
return false;
}
これは、そもそも「認証ロジック」がLaravel管理外の別モジュールにある場合などに使うもので、それがLaravel標準の認証ロジックよりもセキュリティが強固だ!セキュリティホールも絶対にないしあってもすぐに対応するぜ!という自信と責任感がある場合のみ行ってください😋
解説
Laravelの認証周りはカスタマイズ方法がガッツリと用意されています。
さらに、その認証ステップの各段階に、どのクラスのどのメソッドがどんな役割を担っているかも
しっかり決まっていて細かく別れています。
今回の「ログイン」でいうと、下記のような感じ。
- 入力された情報(Request)から、ユーザーを探す条件(credentials)だけ抽出する
- ユーザーを探す条件を元に、ユーザーモデルのインスタンス(App\Userなど)を取得する
- そのユーザーモデルがログインすることができるかをチェックする
それぞれ、
- LoginController@credentials
- UserProvicer@retrieveByCredentials
- UserProvider@validateCredentials
となっています。
このステップをそのまま、3通りのカスタマイズ方法として紹介させていただきました。
以下、その標準コードと役割の解説。
LoginController@credentials
標準では Illuminate\Foundation\Auth\AuthenticatesUsers
がトレイトとして機能を提供、 LoginController に組み込まれています。
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password');
}
public function username()
{
return 'email';
}
リクエスト $request から email と password を抜き取って配列にしているだけです。
username()メソッドは、ログインIDが入っているカラムをオーバーライドして変更するためのメソッドですね。
UserProvicer@retrieveByCredentials
標準だと Illuminate\Auth\EloquentUserProvider
です。
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
重要なのは後半で、 credentilals を ->where( $key , $value )
とシンプルにクエリに変換しています。
なので、App\User にカラムがあってそれをANDで連結する限り、
先の credentials メソッドをカスタマイズするだけで条件が増やせます。
UserProvider@validateCredentials
UserProviderにはいろんな機能を載せることができて、その最も重要なメソッドがコレ。
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
Hash::check
でパスワードチェックをしています。ここは触らずにおきましょー。
感想
全然関係ないけど、最近になってようやく「コントローラ」とそれ以外の役割分担がわかってきました。
コントローラ ... HTTPリクエストを受け取り、必要な機能を呼ぶ
それ以外 ... 提供したい機能に必要な最小限のパラメータを受け取り、結果を最小限で返す
なので、コントローラのコードは、Request $request
を受け取って、それをバラして、必要な情報だけ選択して、サービスを呼び出すだけ。
$reqeustはコントローラから外には出さない。
そうした考え方が「ログインに必要な情報はEMAILとPASSWORDだけだからそれを抜き取ってくるのがログインコントローラ」というのがLaravel標準でもしっかり実装されていて、あー自分のこの考え方は合ってるんだなーと実感した次第。
このあたりはまた機会を改めて記事にできればと思っています。
こんな記事も書いています
Laravelのちょっとマニアックな視点から、誰も書かない記事を書いています(笑
合わせてご覧いただけると幸いです(^^)