-
アカウントロック機能の実装
##はじめに
アカウントをロック(ログインフォームの利用を不可能に)するのですから、ユーザーを特定しなければなりません。 ロックできるのは、「メールアドレス」はあっているが、「パスワード」が間違っている場合 にのみ機能するものです。
ログイン機能については、 「悪意のあるユーザー」と「何度もパスワードを間違った正規のユーザー」それぞれに適した実装 を心がけなければなりません。
##要件
- 規定回数以上ログインに失敗した場合、「アカウントをロックしました」というメッセージを画面に表示し、ログインボタンおよび入力フォームを無効にする。
- メールアドレスがあっているがパスワード不一致でログインできないことが、規定回数以上になった場合、アカウントがロックされたことを、登録されたメールアドレスあてにメールを送信する。
- アカウントロックのメールには、設定時間が過ぎれば、通常通りログインできる旨を明記し、すぐにロックを解除したい場合は、解除方法も通知する。
##実装
###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;
}
}