Laravel は PHP のフレームワークで認証機能、ルーティング、エラーハンドリング、データベースのモデリングなどなど様々なものが備わっており web サイトや web アプリケーションを作る時に便利です。作ったプログラムの機能や取り扱うものによっては自前でログイン機能を持つ必要があります。また、そういった複雑であったりセキュアなものが要求される案件程 Laravel が用意した認証機能のスカフォールドでは要件を満たせないことが多いです。満たせない部分は主にログイン処理です。ログインしている or していない、やログインしているユーザの情報の参照などは非常に便利でそのまま使いやすいですが、素で用意されているIDを送って password をキーにパスワードを送るのみのログイン機能は使えないことが多いです。そこで認証の真偽を自前で用意して認証結果を Laravel に渡す必要があります。
Laravel では自前でログイン機能を用意するための手引きも多少用意されています。
認証 6.x Laravel#自前のユーザー認証
自前でログイン機能を用意する前に Laravel のログインスカフォールドである \Illuminate\Foundation\Auth\AuthenticatesUsers でサポートしてくれている部分を確認しておく必要があります。セキュリティに完璧な認証機能を作ることは難易度が高く、ログイン機能に限ってもなかなかのものです。
Laravel のログイン機能の肝は次のコードです。この login メソッドに POST でアクセスさせるのがスカフォールドのデフォルトです。
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request)
{
// ログインのリクエストとして適格かをバリデートします
$this->validateLogin($request);
// クラスが ThrottlesLogins trait を use している場合、
// このアプリケーションのログイン試行を自動的にスロットル化することができます。
// このアプリケーションへのリクエストを行うクライアントのユーザ名と IP アドレスをキーにします。
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
// ThrottlesLogins trait では多量のログイン試行リクエストを防ぎます。これは総当たり攻撃への対策となります。
// ↑で method_exists($this, 'hasTooManyLoginAttempts') とやっていますが
// この trait 内で hasTooManyLoginAttempts を持つ ThrottlesLogins trait を use していますので確実にこの hasTooManyLoginAttempts が存在します。
return $this->sendLockoutResponse($request);
}
// ログインが通ればログイン成功レスポンスを返します
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// ログイン試行が失敗した場合は、ログイン試行回数を増やし、
// ログインフォームにリダイレクトします。
// もちろん、このユーザが最大試行回数を超えるとロックアウトされます。
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($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',
]);
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function attemptLogin(Request $request)
{
// リクエストの内容を guard に渡してログインを試みます
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
ログインの前処理として行っているのはバリデーションとアクセス回数制限です。バリデーション抜きにログイン処理を行うのはバグの元となって危険であり、アクセス回数制限を行わないのは総当たり攻撃を受けることになるのでどちらも必要です。ログインの前処理が終わったならばリクエストを元にログインに適したリクエストか否かのチェックと適用をしています。
これを受けてお手製認証機能でログインでき、セキュリティを Laravel 相当に担保したコードが次です。
// ログイン回数制限処理が詰まっている Laravel 製 trait
use \Illuminate\Foundation\Auth\ThrottlesLogins;
/**
* お手製ログイン処理
* @param \Illuminate\Http\Request $request
* @throws \Illuminate\Validation\ValidationException
* @return \Illuminate\Http\RedirectResponse
*/
public function login(Request $request)
{
// ここでは紙面の都合でバリデータを使っていますがただのリクエストの代わりにフォームリクエストを使うのも手です。
Validator::validate($request->post(), [
'admin_id' => 'required|string', // ログインしたい管理者のID
'password' => 'required|string', // パスワード
'app_space' => 'required|string', // Slack 的なアプリ独自のキー
'remember' => 'nullable|boolean', // 継続ログインをするか否か
]);
// 認証に必要なパラメータが揃っていないと↑でバリデーション例外が投げられます
// ThrottlesLogins::hasTooManyLoginAttempts では多量のログイン試行リクエストを防ぎます。これは総当たり攻撃への対策となります。
if ($this->hasTooManyLoginAttempts($request)) {
return redirect('ログインページ');
}
/**
* 独自のクエリでログインに挑戦する管理者モデルを取得.
* 大体ここが要件によって大きく変わります。
* @var Admin $admin
*/
$admin = Admin::where(なんやかんや)->first();
// ここでは PHP 組み込みのパスワード認証機能を使います。
// これを使う場合パスワードのハッシュ化には password_hash(平文パスワード,PASSWORD_DEFAULT) を使うのがベターです
// やんごとなき理由で独自のハッシュ化と照合処理を入れる時はまた別のものになります。
if (password_verify($request->password, $admin->password)) {
// パスワード認証が通れば先に取得した管理者モデルインスタンスを Laravel の認証機能に渡します
// SessionGuard を使っている場合、これで Laravel の管理するセッション内にログイン情報が保持されます。
// remember に true or false で継続ログインをするか否かを定められます
Auth::guard('admin')->login($admin, (bool) $request->remember);
// ログイン成功時のレスポンスです。
return redirect('管理画面インデックスページ');
}
// ログイン試行回数を +1 します
$this->incrementLoginAttempts($request);
// ログイン失敗時のレスポンスです。
return redirect('ログインページ');
}
バリデーションを独自、ログイン挑戦回数の制限を Laravel そのまま、ログインOKかの判断を独自、ログイン後のユーザ情報を Laravel そのまま、レスポンスを独自、としたコードです。このようにするとスカフォールド、ガード、プロバイダの内部実装を読みつくさなくとも安全で便利な Laravel 上のログイン機能を書けます。