Laravel5.4、ログイン時の認証情報の追加方法

タグ: Laravel5.3   Laravel5.4  

Laravelの認証はそれだけで、それなりに便利です。ログイン失敗が多ければ、一定時間試行させないなど機能は揃っています。もちろん、足りなければ追加することができます。

実際の認証処理では、ユーザーのアカウントをメールアカウントの後に有効にしたり、イタズラが過ぎるユーザーのアカウントを停止したりはよくある要求です。今回はusersテーブルにactiveフィールドを追加し、それが無効(0)の場合はログインをさせない仕様に変更してみましょう。(有効は1、無効は0としましょう。)

今回の記事も、「検索してわからなければ、コードを読んでみましょう」運動推進する意図を持っています。多分、5.3でも動作すると思います。

認証条件を付け足す

auth:make Artisanコマンドで生成されるLoginControllerでは、ユーザーを一意に特定する項目はメールアドレス(email)です。内部的にメールアドレスに一致するレコードを引っ張り、そのBcryptハッシュ済みpassword項目と、ページ入力された値をHashクラスで比較し、一致していればログイン成功と判断します。

userテーブルをセレクトする際に、他の項目を追加で指定できます。比較する項目を指定しているのは、LoginControllerで使用されているAuthenticatesUsersトレイトのcredentialsメソッドです。このメソッドは、$request->only( $this->username(), 'password' )を返すだけです。リクエストに含まれる入力フィールド値のうち、一意に絞り込むために必要なemailと、比較に必要なpasswordだけを配列として返すためにonlyメソッドが使われています。

$this->username()emailを返しています。"username"はもちろんユーザー名のことです。テーブル上のユーザー名はnameですが、一意に絞り込む項目という意味で"username"というメソッド名を使用しています。このメソッドをオーバーライドし、nameのようなemail以外の項目で一意に絞りこめるように指定することも可能です。今回は、最近のWebサービスで多く使用されているemailを利用することにし、デフォルトのまま使用しています。

以下が、変更するコードとなります。実際の変更コードはreturnのある行のみです。

    /**
     * リクエストから認証に必要な情報を選択する.
     *
     * 返す配列に追加することで、認証条件を付け加えられる。
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return array
     */
    protected function credentials(Request $request)
    {
        // オリジナルのコード
        // return $request->only( $this->username(), 'password' );

        return array_merge($request->only($this->username(), 'password'), ['active' => '1']);
    }

単に、activeフィールドが1という配列をマージしているだけです。これで、userテーブルのセレクト時にACTIVE=1という条件が付け加えられ、もしactiveが1以外であれば、emailpasswordが合っていても、レコードは取得できません。

ただし、レコードが取得できないために、メールアドレスに対する「ログイン情報が登録されていません。」という旨のメッセージになります。この振る舞いは、AuthenticatesUsersトレイトのsendFailedLoginResponseメソッドでコードされています。

もしこの方法を取るのであれば、auth原語ファイルの翻訳行を変更し、わかりやすくしないとユーザーに対して不親切です。resources/lang/ja/auth.phpに英語版を翻訳したものを入れている場合、以下のようになるでしょう。

return [

    /*
    |--------------------------------------------------------------------------
    | 認証言語行
    |--------------------------------------------------------------------------
    |
    | 以下の言語行は認証時にユーザーに対し表示する必要のある
    | 様々なメッセージです。アプリケーションの必要に合わせ
    | 自由にこれらの言語行を変更してください。
    |
    */

    // 以下の`failed`言語行をわかりやすく変更する。
    'failed' => 'ログイン情報が登録されていません。もしくは、有効になっていません。登録直後の方は、確認メール中のリンクをクリックし、アカウントを有効にしてください。',
    'throttle' => 'ログインに続けて失敗しています。:seconds秒後に再度お試しください。',

];

自前で実装

前記の方法は修正箇所が少ないため手軽です。しかし、email項目に対して長いメッセージが出てきてしまうのが欠点です。長いメッセージに2つの内容が含まれてしまいます。

もし、全体で表示されるレイアウトにメッセージ表示エリアを含めているのであれば、「確認メールでアクティベイトしてほしい」というメッセージは、そちらへ表示したいところです。

@if (session()->has('alert'))
    <div>
        <p style="color:red">{{ session()->get('alert') }}</p>
    </div>
@endif

上記のコードは、レイアウトでメッセージを表示する一例です。セッションにalertキーの項目が存在している場合、それを表示します。特定の項目に対するものではない、いわゆる普通のエラーメッセージを表示する場合ですね。ページの上部とか、最近はJavaScriptでページ下の方へ10秒程度表示したりしますね。

これを実現するため、ドキュメントで説明されている通り、Auth::attemptメソッドを使い、自前でログイン処理することも簡単です。しかし、試行回数オーバーによるログインのロックなど、LoginController(実際は、AuthenticatesUsersトレイトやThrottlesLoginsトレイト)で実装されている機能が利用できなくなります。(もちろん、各トレイトをuseし、実装することもできます。)

そこで、再度LoginControllerで、AuthenticatesUsersトレイトで定義されているメソッドをオーバーライドすることで、上記のエラーメッセージ表示エリアへ、「アクティベイトしてね」旨のメッセージ表示を実現してみたいと思います。

オーバーライドするのはloginメソッドです。このメソッドは、POSTメソッドのlogin URIに対するルートでアクションメソッドに指定されています。平たく言えば、/loginへフォームが送信(POST)されると、呼び出されるメソッドです。

LoginControllerAuthenticatesUsersloginメソッドをコピペし、ロジックを追加したのが以下になります。もともとのコメントは英語ですから、短く付けなおしてあります。

    public function login(Request $request)
    {
        // 入力のバリデーション
        $this->validateLogin($request);

        // ログインに何回も失敗するとログインできなくなる仕組み
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

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

        // 実際のログインのチェック
        if ($this->attemptLogin($request)) {
            // ログイン成功、以下はオリジナルコード
            // return $this->sendLoginResponse($request);

            // 以下のif文のロジックを追加

            // この時点でログイン済みユーザの情報はAuthで保持されているが、
            // リクエストインスタンスには存在しない
            if (auth()->user()->active == false) {

                // アクティブではないので、ログアウトさせる
                // このメソッドの返却値は'/`へリダイレクトさせるので、無視する
                $this->logout($request);

                // 改めて、今回は直前のURI(ログインページ)へリダイレクト
                // withでフラッシュメッセージとしてエラーメッセージをセッションへ保存
                return redirect()->back()
                    ->withInput()
                    ->with('alert', 'アカウントが有効になっていません。確認メール中のリンクをクリックし、有効にしてください。');
            }

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

        // ログイン失敗、なので試行回数をインクリメント
        $this->incrementLoginAttempts($request);

        // ログイン失敗のレスポンス生成
        return $this->sendFailedLoginResponse($request);

実際には、ログイン成功時のロジックを変更しています。オリジナルではコメントの通り、sendLoginResponseメソッドの返却値をそのままリターンしているだけです。これは、ログイン成功後の適切な箇所へリダイレクトさせるレスポンスインスタンスです。

このレスポンスインスタンスを返す前に、activeが有効であることを確認します。ログインに成功しているため、Authのインスタンスには、ユーザーの情報、デフォルトではUser Eloquentモデルが保持されています。通常、リクエストインスタンスからユーザー情報は取得できますが、今回はログインのためのリクエストですから、そこには「ログイン済み」を表すユーザー情報が含まれないのは、意味が通ります。

auth()ヘルパー関数で、取得済みの情報(Userモデルインスタンス)へアクセスし、余計なクエリーを出さないようにしています。

activeが無効を表す0の場合をコードの意味がわかりやすいため== falseで尋ねていますが、考え方によっては== 0=== 0で判定したほうが良いでしょう。

もし無効の場合は、既にログイン状態ですので、まずログアウト処理しています。$this->logoutメソッドは、ルートページ(/ URI)へのリダイレクトレスポンスを返してきます。それは無視し、今回はbackメソッドで直前のページ、つまりログインページにリダイレクトさせています。activeではない状態ですから、/へリダイレクトさせても良いでしょうし、詳細な説明ページを用意し、そこへリダイレクトする方法もあるでしょう。

リダイレクトに対するwithInputメソッドは、入力コードを全てフラッシュデータとしてセッションへ保存してくれます。フラッシュデータとは、次の一回のセッション、つまりリダイレクト後のセッションのみセッションに存在するデータです。そのセッションが終わると自動的に消去され、セッションへ残りません。主にメッセージや入力項目の再表示に使用されます。

リダイレクトに対するwithメソッドは、その後の引数の内容をセッションへフラッシュデータとして保存します。今回はalertキーで、エラーメッセージを保存しています。リダイレクト先でページが表示される際、前記レイアウト中のコード、session()->has('alert')がTRUEになり、メッセージが表示されるわけです。

withはメソッドは動的に処理されているため、このサンプルのコードの場合、->withAlert('メッセージ');と書くこともできます。つまり、withに続くAlertの先頭を小文字にしたalertをキーとして、引数の内容がセッションへフラッシュデータとして保存されます。

まとめ

よくある認証でのログイン条件追加について、解説しました。

コードリーディングをしていくうちに、このような知見が得られます。上級者とはコードリーダーです。少し検索して情報が得られないなら、コードを読んでください。

別の方法として、イベントを捉えリスナーで処理する方法も考えました。リスナー側からイベント発行側へは直接情報を返せないため、レスポンスを返すのは無理です。abort関数を実際に試してみましたが、正しく動作しないようです。もともと、機能を独立させるための機能ですから、しかたありません。

ちなみに、私はMailableクラスのビューでblade記法が動作しなくて半日悩みました。原因は、拡張子がblade.phpになっていなかったという単純なミスでした。まだまだです。