13
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-11-05
  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;
	}

}
13
13
0

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
13
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?