2012年4月9日

Yii Framework: ログインの試行回数を制限する

セキュリティを考えるとログインの試行回数は制限したほうがいいように思います。Twitter ではログインを数回失敗するとスパム対策のためキャプチャの入力を強制するようになっていたり、また、はてななどはログインを数回失敗すると、一定期間ログインできないようになっています。

今回はそういったものを Yii で作ってみます。キャプチャは難しそうなので、まずは数回ログインを失敗すると、一定期間ログインできないような機能を追加してみます。

全体的な流れ

  1. login_attempt テーブルを作成
  2. LoginAttempt モデルを作成
  3. LoginController.php に試行回数の制限を実施するコードを追加
  4. login.php ビューの切り替え

1. login_attempt テーブルを作成

`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(32) NOT NULL,
`login` varchar(32) NOT NULL,
`create_time` int(11) NOT NULL,
`expiration_time` int(11) NOT NULL,

 login カラムは、フォームから入力されたログイン情報が挿入されます。expiration_time は有効期限です。基本的に一定の短い期間に連続してログインに失敗したものを、IPアドレスの判定でログインできないようにする、という形をとります。

2. LoginAttempt モデルを作成

<?php
class LoginAttempt extends ActiveRecord
{
const LOGIN_ATTEMPT_LIMIT = 5; // ログイン試行回数のリミット
const BANNED_IP_EXPIRATION_TIME = '+1 hour'; // この場合は禁止されてから一時間経たないとログインできない
...
/**
* @see CActiveRecord::beforeSave()
*/
protected function beforeSave()
{
$this->create_time = time();
$this->expiration_time = strtotime(self::BANNED_IP_EXPIRATION_TIME, $this->create_time);
return true;
}
/**
* ログインが禁止されたIPアドレスか、そうでないか
* @param string $ip ip address
* @return boolean
*/
public function isBanned($ip)
{
$c = $this->getCriteriaByIp($ip);
if ($this->count($c) >= self::LOGIN_ATTEMPT_LIMIT) {
return true;
}
return false;
}
/**
* ログインが禁止されたIPアドレスを期限切れの場合は削除する
* @param string $ip ip address
*/
public function purgeBannedIp($ip)
{
$c = $this->getCriteriaByIp($ip);
if ($this->count($c) >= self::LOGIN_ATTEMPT_LIMIT) {
$c->order = 't.create_time DESC';
$model = $this->find($c);
if (time() > $model->expiration_time) {
$this->deleteAll('ip = :ip', array(':ip' => $ip));
}
}
}
/**
* IPを条件にしたcriteriaを取得する
* @param string $ip ip address
* @return object criteria
*/
private function getCriteriaByIp($ip)
{
$c = new CDbCriteria;
$c->condition = 't.ip = :ip';
$c->params[':ip'] = $ip;
return $c;
}
...

3. LoginController.php に試行回数の制限を実施するコードを追加

<?php
class LoginController extends Controller
{
private $ip;
/**
* @see CController::defaultAction
*/
public $defaultAction = 'login';
/**
* @see CController::init()
*/
public function init()
{
$this->ip = Yii::app()->request->userHostAddress;
}
/**
* ユーザのログイン
*/
public function actionLogin()
{
if (Yii::app()->user->id) {
$this->redirect('/');
}
$loginAttempt = new LoginAttempt();
$loginAttempt->purgeBannedIp($this->ip);
$isBanned = $loginAttempt->isBanned($this->ip);
if (!$isBanned) {
$model = new Login();
if (isset($_POST['Login'])) {
$model->attributes = $_POST['Login'];
if ($model->validate()) {
$this->redirect(Yii::app()->user->returnUrl);
}
if (!empty($model->login)) {
$loginAttempt->login = $model->login;
$loginAttempt->ip = $this->ip;
$loginAttempt->save(false);
}
}
}
$this->render('/user/login', compact('model', 'isBanned'));
}
}
禁止されている IP かを見て、禁止されていなければログインのフォームを入力できるようになっています (未入力の場合は、カウントしない) 。$isBanned には true (禁止されているIP) もしくは false (禁止されていないIP) の値が入っていて、それをビューに渡して、表示の切り替えも行います。ビューは以下。

4. login.php ビューの切り替え 

<?php if (!$isBanned): ?>
<div class="form">
<?php echo CHtml::form(); ?>
<?php echo CHtml::errorSummary($model, false); ?>
<div class="row">
<?php echo CHtml::activeLabel($model, 'login'); ?>
<?php echo CHtml::activeTextField($model, 'login', array('maxlength' => 64)); ?>
</div><!-- /.row -->
<div class="row">
<?php echo CHtml::activeLabel($model, 'password'); ?>
<?php echo CHtml::activePasswordField($model, 'password', array('maxlength' => 64)); ?>
</div><!-- /.row -->
<div class="row buttons">
<?php echo CHtml::submitButton('ログイン'); ?>
<?php echo CHtml::activeCheckBox($model, 'rememberMe'); ?>
<?php echo CHtml::activeLabel($model, 'rememberMe'); ?>
</div><!-- /.row buttons -->
<?php echo CHtml::endForm(); ?>
</div><!-- /.form -->
<?php else: ?>
<div class="flash-error">
ログイン試行回数が制限を越えてしまいました。時間をおいて、再度ログインを試してください
</div><!-- /.flash-error -->
<?php endif; ?>
view raw login.php hosted with ❤ by GitHub
上記の流れでは、禁止された IP アドレスでも、有効期限が切れればテーブルから削除され、ログインできるようになっていますが、禁止されるまでにはいたっていないデータはテーブルに溜まってしまいます。そういうものに対しては一定期間が過ぎれば削除するコマンドを作って、cron で定期的に実行すれば良いのかな?と今のところ考えています。

参考リンク

0 件のコメント:

コメントを投稿