Help us understand the problem. What is going on with this article?

PHPでログイン機能を実装するチュートリアル #8

More than 3 years have passed since last update.
  1. 基本設計、ユーザーモデル
  2. オートローダー
  3. 例外・ログ
  4. PDO シングルトン SQLインジェクション

  5. ユーザーモデルの作成

  6. クラスの継承

  7. テンプレート・クラスの実装

  8. アカウントロック機能の実装

  9. メール送信機能の実装

  10. アカウントロック解除機能の実装

  11. CSRF対策

はじめに

 アカウントをロック(ログインフォームの利用を不可能に)するのですから、ユーザーを特定しなければなりません。 ロックできるのは、「メールアドレス」はあっているが、「パスワード」が間違っている場合 にのみ機能するものです。

ログイン機能については、 「悪意のあるユーザー」と「何度もパスワードを間違った正規のユーザー」それぞれに適した実装 を心がけなければなりません。

要件

  1. 規定回数以上ログインに失敗した場合、「アカウントをロックしました」というメッセージを画面に表示し、ログインボタンおよび入力フォームを無効にする。
  2. メールアドレスがあっているがパスワード不一致でログインできないことが、規定回数以上になった場合、アカウントがロックされたことを、登録されたメールアドレスあてにメールを送信する。
  3. アカウントロックのメールには、設定時間が過ぎれば、通常通りログインできる旨を明記し、すぐにロックを解除したい場合は、解除方法も通知する。

実装

LoginController.class.php

noticeAccountLockForMail(UserModel $objUserModel); メソッドを追加し、login() メソッド内からコールします。

LoginController.class.php
<?php

namespace MyApp\controller;

use \MyApp\model\UserModel;
use \MyApp\common\Db;
use \MyApp\common\InvalidErrorException;
use \MyApp\common\ExceptionCode;
use \MyApp\common\Log;
use \MyApp\common\Mail;

/**
 * LoginController
 */
class LoginController
{

    /**
     * ログイン成功時の遷移先
     */
    const TARGET_PAGE = '/dashboard.php';

    /**
     * セッションに保存する名前
     */
    const LOGINUSER = 'loginUserModel';

    /**
     * メールアドレスとパスワードでログインする
     * @return void
     */
    static public function login()
    {
        // POSTされていないときは、処理を中断する
        if (!filter_input_array(INPUT_POST)) {
            return;
        }

        //フォームからの値を受け取る
        $email = filter_input(INPUT_POST, 'email');
        $password = filter_input(INPUT_POST, 'password');

        // いずれかが空文字の場合、例外
        if ('' == $email || '' == $password) {
            throw new InvalidErrorException(ExceptionCode::ARGUMENT_REQUIRED);
        }

        //トランザクションを開始する
        Db::transaction();

        //  email から ユーザーモデル を取得する
        $objUserModel = new UserModel();
        $objUserModel->getModelByEmail($email);

        //  ロックされたアカウントかどうかをチェックする
        if ($objUserModel->isAccountLock()) {

            // コミット(ロールバックでも構わない。トランザクションを残さないため。)
            Db::commit();

            //  ロックされている
            throw new InvalidErrorException(ExceptionCode::INVALID_LOCK);
        }

        //パスワードチェック
        if (!$objUserModel->checkPassword($password)) {

            // ログイン失敗記録
            $objUserModel->loginFailureIncrement();

            // コミット(失敗回数を書き込むので)
            Db::commit();

            // アカウントロック通知
            self::noticeAccountLockForMail($objUserModel);

            // アカウントロック通知(Cookie)
            self::noticeAccountLockForCookie();

            // ログインに失敗しました。
            throw new InvalidErrorException(ExceptionCode::INVALID_LOGIN_FAIL);
        }

        //  ログイン失敗をリセット
        $objUserModel->loginFailureReset();

        //コミット
        Db::commit();

        //セッション固定攻撃対策
        session_regenerate_id(true);

        //セッションに保存
        $_SESSION[self::LOGINUSER] = $objUserModel;

        //ページ遷移
        header(sprintf("location: %s", self::TARGET_PAGE));
    }

    /**
     * アカウントロック・メール通知
     * @param UserModel $objUserModel
     * @return void
     */
    private static function noticeAccountLockForMail(UserModel $objUserModel)
    {
        // 規定回数以内のとき、なにもしない。
        if (UserModel::LOCK_COUNT > $objUserModel->getLoginFailureCount()) {
            return;
        }

        // メール通知
        $strRecipient = $objUserModel->getEmail();
        $strSubject = 'アカウントをロックしました。';
        $strBody = 'とりあえず空';
        Mail::send($strRecipient, $strSubject, $strBody);

        throw new InvalidErrorException(ExceptionCode::INVALID_LOCK);
    }

}

UserModel.class.php

UserModel.class.php
<?php

namespace MyApp\model;

use MyApp\dao\UserDao;

/**
 * UserModel
 */
final class UserModel extends UserModelBase
{

    /**
     * アカウントをロックするログイン失敗回数
     */
    const LOCK_COUNT = 3;

    /**
     * アカウントをロックする時間(分)
     */
    const LOCK_MINUTE = 15;

    /**
     * メールアドレスからユーザーを検索する
     * @param string $strEmail
     * @return \MyApp\model\UserModel
     */
    public function getModelByEmail($strEmail)
    {
        $dao = UserDao::getDaoFromEmail($strEmail);
        return (isset($dao[0])) ? $this->setProperty(reset($dao)) : null;
    }

    /**
     * パスワードが一致しているかどうかを判定する
     * @param type $password
     * @return bool
     */
    public function checkPassword($password)
    {
        $hash = $this->getPassword();
        return password_verify($password, $hash);
    }

    /**
     * ログイン失敗をリセットする
     * 1以上のときに0にする
     * @return bool
     */
    public function loginFailureReset()
    {
        $count = $this->getLoginFailureCount();
        if (0 < $count) {
            $this->setLoginFailureCount(0)
                ->setLoginFailureDatetime(null);
            return $this->save();
        }
        //変更の必要がない
        return true;
    }

    /**
     * ログイン失敗をインクリメントする
     * 指定回数(self::LOCK_COUNT)に満たないときのみ+1
     * @return bool
     */
    public function loginFailureIncrement()
    {
        $count = $this->getLoginFailureCount();
        if (self::LOCK_COUNT > $count) {
            $now = (new \DateTime())->format('Y-m-d H:i:s');
            $this->setLoginFailureCount(1 + $count)
                ->setLoginFailureDatetime($now);
            return $this->save();
        }

        //ログイン失敗が設定以上のとき
        return true;
    }

    /**
     * アカウントがロックされているかどうかを判定する
     * @return bool ロックされていたら true
     */
    public function isAccountLock()
    {
        $count = $this->getLoginFailureCount();
        $datetime = $this->getLoginFailureDatetime();

        $lastFailureDatetime = new \DateTime($datetime);
        $interval = new \DateInterval(
            sprintf('PT%dM', self::LOCK_MINUTE)
        );
        $lastFailureDatetime->add($interval);

        //設定時間以内で、かつ設定回数以上の失敗を記録しているとき
        if ($lastFailureDatetime > new \DateTime() && self::LOCK_COUNT <= $count) {
            return true;
        }
        return false;
    }

}
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away