LoginSignup
14
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-25
  1. 基本設計、ユーザーモデル
  2. オートローダー
  3. 例外・ログ
  4. PDO シングルトン SQLインジェクション

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

  6. クラスの継承

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

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

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

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

  11. CSRF対策

PDO シングルトン SQLインジェクションのときに実装していなかった UserModel 及び LoginController のメソッドを定義してゆきます。

前回作成したデータベース・クラスのメソッドは必要な場所に、追記しております。

LoginController
<?php

namespace MyApp\controller;

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

/**
 * UserController
 */
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) {
            return;
        }

        //トランザクションを開始する
        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();
            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));
    }

    /**
     * ログインしているかどうかチェックする
     * @return bool
     */
    static public function checkLogin()
    {
        $objUserModel = (isset($_SESSION[self::LOGINUSER])) ?
            $_SESSION[self::LOGINUSER] :
            null;

        if (is_object($objUserModel) && 0 < $objUserModel->getUserId()) {
            return;
        }
        header('Location: /');
    }

    /**
     * ログイン中のユーザーモデルを取得する
     * @return UserModel
     */
    static public function getLoginUser()
    {
        return $_SESSION[self::LOGINUSER];
    }

    /**
     * ログアウトする
     * @return void
     */
    static public function logout()
    {
        $_SESSION = [];
        session_destroy();
        header('Location: /');
    }

}

ExceptionCode.class.php
<?php

namespace MyApp\common;

/**
 * ExceptionCode.class.php
 * @since 2015/07/25
 */
class ExceptionCode
{

    const INVALID_ERR = 1000;
    const INVALID_LOCK = 1001;
    const INVALID_LOGIN_FAIL = 1002;
    const APPLICATION_ERR = 2000;
    const SYSTEM_ERR = 3000;
    const TEMPLATE_ERR = 3001;

    private static $_arrMessage = array(
        self::INVALID_ERR => 'エラーが発生しました。'
        , self::INVALID_LOCK => 'アカウントがロックされています。'
        , self::INVALID_LOGIN_FAIL => 'ログインに失敗しました。'
        , self::APPLICATION_ERR => 'アプリケーション・エラーが発生しました。'
        , self::SYSTEM_ERR => 'システム・エラーが発生しました。'
        , self::TEMPLATE_ERR => 'テンプレート[%s]が見つかりません。'
    );

    static public function getMessage($intCode)
    {
        if (array_key_exists($intCode, self::$_arrMessage)) {
            return self::$_arrMessage[$intCode];
        }
    }

}

基本設計、ユーザーモデル で作成した、UserModel.class.php に以下のメソッドを作成します。SQLが書かれる UserDao.class.php も合わせて、記述します。

  • getModelByEmail($email)
  • isAccountLock()
  • checkPassword($password)
  • loginFailureIncrement()
  • loginFailureReset()
  • save()
  • setPropaties()
UserModel.class.php
<?php

namespace MyApp\model;

use MyApp\dao\UserDao;

/**
 * UserModel
 */
final class UserModel
{

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

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

    /**
     * ユーザーID
     */
    private $_userId = null;

    /**
     * パスワード(ハッシュ)
     */
    private $_password = null;

    /**
     * 表示名
     */
    private $_displayName = null;

    /**
     * メールアドレス
     */
    private $_email = null;

    /**
     * トークン
     */
    private $_token = null;

    /**
     * ログイン失敗回数
     */
    private $_loginFailureCount = null;

    /**
     * ログイン失敗日時
     */
    private $_loginFailureDatetime = null;

    /**
     * 削除フラグ
     */
    private $_deleteFlag = null;

    /**
     * メールアドレスからユーザーを検索する
     * @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;
    }

    /**
     * プロパティをセットする
     * @param array $arrDao
     * @return \MyApp\model\UserModel
     */
    private function setProperty(array $arrDao)
    {
        $this->setUserId($arrDao['userId'])
            ->setDisplayName($arrDao['displayName'])
            ->setEmail($arrDao['email'])
            ->setPassword($arrDao['password'])
            ->setToken($arrDao['token'])
            ->setLoginFailureCount($arrDao['loginFailureCount'])
            ->setLoginFailureDatetime($arrDao['loginFailureDatetime'])
            ->setDeleteFlag($arrDao['deleteFlag']);
        return $this;
    }

    /**
     * 更新する
     * @return bool
     */
    public function save()
    {
        return UserDao::save($this);
    }

    /**
     * 新規作成する
     * @return bool
     */
    public function create()
    {
        return UserDao::insert($this);
    }

    /**
     * ユーザーIDを設定する
     * @param int $userId
     * @return \MyApp\model\UserModel
     */
    public function setUserId($userId)
    {
        $this->_userId = $userId;
        return $this;
    }

    /**
     * パスワード(ハッシュ)を設定する
     * @param string $password
     * @return \MyApp\model\UserModel
     */
    public function setPassword($password)
    {
        $this->_password = $password;
        return $this;
    }

    /**
     * 表示名を設定する
     * @param string $displayName
     * @return \MyApp\model\UserModel
     */
    public function setDisplayName($displayName)
    {
        $this->_displayName = $displayName;
        return $this;
    }

    /**
     * メールアドレスを設定する
     * @param string $email
     * @return \MyApp\model\UserModel
     */
    public function setEmail($email)
    {
        $this->_email = $email;
        return $this;
    }

    /**
     * トークンを設定する
     * @param string $token
     * @return \MyApp\model\UserModel
     */
    public function setToken($token)
    {
        $this->_token = $token;
        return $this;
    }

    /**
     * ログイン失敗回数を設定する
     * @param int $loginFailureCount
     * @return \MyApp\model\UserModel
     */
    public function setLoginFailureCount($loginFailureCount)
    {
        $this->_loginFailureCount = $loginFailureCount;
        return $this;
    }

    /**
     * ログイン失敗日時を設定する
     * @param string $loginFailureDatetime
     * @return \MyApp\model\UserModel
     */
    public function setLoginFailureDatetime($loginFailureDatetime)
    {
        $this->_loginFailureDatetime = $loginFailureDatetime;
        return $this;
    }

    /**
     * 削除フラグを設定する
     * @param bool $deleteFlag
     * @return \MyApp\model\UserModel
     */
    public function setDeleteFlag($deleteFlag)
    {
        $this->_deleteFlag = $deleteFlag;
        return $this;
    }

    /**
     * ユーザーIDを取得する
     * @return int
     */
    public function getUserId()
    {
        return $this->_userId;
    }

    /**
     * パスワード(ハッシュ)を取得する
     * @return string
     */
    public function getPassword()
    {
        return $this->_password;
    }

    /**
     * 表示名を取得する
     * @return string
     */
    public function getDisplayName()
    {
        return $this->_displayName;
    }

    /**
     * メールアドレスを取得する
     * @return string
     */
    public function getEmail()
    {
        return $this->_email;
    }

    /**
     * トークンを取得する
     * @return string
     */
    public function getToken()
    {
        return $this->_token;
    }

    /**
     * ログイン失敗回数を取得する
     * @return int
     */
    public function getLoginFailureCount()
    {
        return $this->_loginFailureCount;
    }

    /**
     * ログイン失敗日時を取得する
     * @return string
     */
    public function getLoginFailureDatetime()
    {
        return $this->_loginFailureDatetime;
    }

    /**
     * 削除フラグを取得する
     * @return bool
     */
    public function getDeleteFlag()
    {
        return $this->_deleteFlag;
    }

}

UserDao.class.php
<?php

namespace MyApp\dao;

use MyApp\common\Db;
use MyApp\model\UserModel;

/**
 * UserDao.class.php
 */
class UserDao
{

    /**
     * ユーザーIDから配列を取得する
     * @param type $intUserId
     * @return array
     */
    public static function getDaoFromUserId($intUserId, $intDeleteFlag = null)
    {
        $sql = "SELECT ";
        $sql .= "`userId`";
        $sql .= ", `password`";
        $sql .= ", `displayName`";
        $sql .= ", `email`";
        $sql .= ", `token`";
        $sql .= ", `loginFailureCount`";
        $sql .= ", `loginFailureDatetime`";
        $sql .= ", `deleteFlag` ";
        $sql .= "FROM `tbl_user` ";
        $sql .= "WHERE `userId` = :userId ";

        $arr = array();
        $arr[':userId'] = $intUserId;
        if (!is_null($intDeleteFlag) && in_array($intDeleteFlag, array(0, 1))) {
            $sql .= "AND `deleteFlag` = :deleteFlag ";
            $arr[':deleteFlag'] = $intDeleteFlag;
        }

        return Db::select($sql, $arr);
    }

    /**
     * メールアドレスから配列を取得する
     * @param type $strEmail
     * @return array
     */
    public static function getDaoFromEmail($strEmail, $intDeleteFlag = null)
    {
        $sql = "SELECT ";
        $sql .= "`userId`";
        $sql .= ", `password`";
        $sql .= ", `displayName`";
        $sql .= ", `email`";
        $sql .= ", `token`";
        $sql .= ", `loginFailureCount`";
        $sql .= ", `loginFailureDatetime`";
        $sql .= ", `deleteFlag` ";
        $sql .= "FROM `tbl_user` ";
        $sql .= "WHERE `email` = :email ";

        $arr = array();
        $arr[':email'] = $strEmail;
        if (!is_null($intDeleteFlag) && in_array($intDeleteFlag, array(0, 1))) {
            $sql .= "AND `deleteFlag` = :deleteFlag ";
            $arr[':deleteFlag'] = $intDeleteFlag;
        }

        return Db::select($sql, $arr);
    }

    /**
     * 更新する
     * @param UserModel $objUserModel
     * @return bool
     */
    public static function save(UserModel $objUserModel)
    {
        $sql = "UPDATE ";
        $sql .= "`tbl_user` ";
        $sql .= "SET ";
        $sql .= "`password` = :password ";
        $sql .= ", `displayName` = :displayName ";
        $sql .= ", `email` = :email ";
        $sql .= ", `token` = :token ";
        $sql .= ", `loginFailureCount` = :loginFailureCount ";
        $sql .= ", `loginFailureDatetime` = :loginFailureDatetime ";
        $sql .= ", `deleteFlag` = :deleteFlag ";
        $sql .= "WHERE `userId` = :userId ";

        $arr = array();
        $arr[':userId'] = $objUserModel->getUserId();
        $arr[':password'] = $objUserModel->getPassword();
        $arr[':displayName'] = $objUserModel->getDisplayName();
        $arr[':email'] = $objUserModel->getEmail();
        $arr[':token'] = $objUserModel->getToken();
        $arr[':loginFailureCount'] = $objUserModel->getLoginFailureCount();
        $arr[':loginFailureDatetime'] = $objUserModel->getLoginFailureDatetime();
        $arr[':deleteFlag'] = $objUserModel->getDeleteFlag();

        return Db::update($sql, $arr);
    }

    /**
     * 新規作成する
     * @return int
     */
    public static function insert(UserModel $objUserModel)
    {
        $sql = "INSERT INTO ";
        $sql .= "`tbl_user` ";
        $sql .= "(";
        $sql .= "`userId`";
        $sql .= ", `password`";
        $sql .= ", `displayName`";
        $sql .= ", `email`";
        $sql .= ", `token`";
        $sql .= ", `loginFailureCount`";
        $sql .= ", `loginFailureDatetime`";
        $sql .= ", `deleteFlag`";
        $sql .= ") VALUES (";
        $sql .= "NULL ";
        $sql .= ", :password ";
        $sql .= ", :displayName ";
        $sql .= ", :email ";
        $sql .= ", :token ";
        $sql .= ", :loginFailureCount ";
        $sql .= ", :loginFailureDatetime ";
        $sql .= ", :deleteFlag ";
        $sql .= ")";

        $arr = array();
        $arr[':password'] = $objUserModel->getPassword();
        $arr[':displayName'] = $objUserModel->getDisplayName();
        $arr[':email'] = $objUserModel->getEmail();
        $arr[':token'] = $objUserModel->getToken();
        $arr[':loginFailureCount'] = $objUserModel->getLoginFailureCount();
        $arr[':loginFailureDatetime'] = $objUserModel->getLoginFailureDatetime();
        $arr[':deleteFlag'] = $objUserModel->getDeleteFlag();

        return Db::insert($sql, $arr);
    }

}
14
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12