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


1. [基本設計、ユーザーモデル](http://qiita.com/ShibuyaKosuke/items/f114ffccf441edb2b745)
2. [オートローダー](http://qiita.com/ShibuyaKosuke/items/6109810464a7293293bd)
3. [例外・ログ](http://qiita.com/ShibuyaKosuke/items/88b1305a4ff0c5f3e728)
4. [PDO シングルトン　SQLインジェクション](http://qiita.com/ShibuyaKosuke/items/094df7f399912ae7df56)
	* [汎用データベース・クラス作成](http://qiita.com/ShibuyaKosuke/items/8502492ac024362b528d)

5. [ユーザーモデルの作成](http://qiita.com/ShibuyaKosuke/items/81c556d836d70a29c5f3)
6. [クラスの継承](http://qiita.com/ShibuyaKosuke/items/490ffa6a92670cf6c644)
7. [テンプレート・クラスの実装](http://qiita.com/ShibuyaKosuke/items/803dc58a23601118942e)
8. [アカウントロック機能の実装](http://qiita.com/ShibuyaKosuke/items/e890becdb8193d3def05)
9. [メール送信機能の実装](http://qiita.com/ShibuyaKosuke/items/78c8673782ebbf51584b)
10. [アカウントロック解除機能の実装](http://qiita.com/ShibuyaKosuke/items/ee4b1896071eeca37628)
11. [CSRF対策](http://qiita.com/ShibuyaKosuke/items/8100e304e4447e44e98c)

[PDO シングルトン　SQLインジェクション](http://qiita.com/ShibuyaKosuke/items/094df7f399912ae7df56)のときに実装していなかった `UserModel`　及び `LoginController` のメソッドを定義してゆきます。

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

```php: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: /');
	}

}

```

```php: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];
		}
	}

}
```


[基本設計、ユーザーモデル](http://qiita.com/ShibuyaKosuke/items/f114ffccf441edb2b745) で作成した、`UserModel.class.php` に以下のメソッドを作成します。SQLが書かれる `UserDao.class.php` も合わせて、記述します。

* `getModelByEmail($email)` 
* `isAccountLock()`
* `checkPassword($password)`
* `loginFailureIncrement()`
* `loginFailureReset()`
* `save()`
* `setPropaties()`

```php: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);
	}

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

	/**
	 * ログイン失敗をインクリメントする
	 * 指定回数（self::LOCK_COUNT）に満たないときのみ＋１
	 * @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;
	}

}

```

```php: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);
	}

}
```