12
14

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でログイン機能を実装するチュートリアル #10

Last updated at Posted at 2016-11-06
  1. 基本設計、ユーザーモデル

  2. オートローダー

  3. 例外・ログ

  4. PDO シングルトン SQLインジェクション

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

  6. クラスの継承

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

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

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

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

  11. CSRF対策

前回の続きです。

##はじめに

要件をまとめます。

  1. アカウントがロックされたとき、ロックされたアカウントのユーザー宛てに、メールを送信する。
  2. アカウントロック解除ページのURLはメールで通知する。
  3. アカウントロック解除ページの「解除」ボタンを押下すると、アカウントロックを解除する。

アカウントロックは、当然ログインに成功していないユーザーが利用する機能ですので、セッションを用いてユーザーを特定することはできません。
そこで、「解除コード」のようなキーを用いてユーザーを特定することになります。

「解除コード」はアカウントがロックされたタイミングで発行し、DBに保存しておく必要があります。
UserModel.class.php に定義されている、loginFailureIncrement() にその処理を足しておきましょう。

tbl_userテーブルのtokenにその値を書き込みます。

classes/model/UserModel.class.php
	/**
	 * ログイン失敗をインクリメントする
	 * 指定回数(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);

			// 追加
			if (self::LOCK_COUNT === 1 + $count) {
				$token = sha1(uniqid());
				$this->setToken($token);
			}

			return $this->save();
		}

		//ログイン失敗が設定以上のとき
		return true;
	}

##HTMLの作成

最初に、アカウントロック解除ページ(unlock.php)を作成します。

smarty/template/unlock.tpl
{block name='meta'}
	<style type="text/css">
		.login-box, .register-box {
			margin: 20px auto;
		}
	</style>
{/block}
{block name='content'}
	<div class="login-box">
		<div class="login-logo">
			<a href="/">
				<b>Admin</b>
				LTE
			</a>
		</div>
		<!-- /.login-logo -->
		<div class="login-box-body">

			<p class="login-box-msg">ロックを解除するには、下のボタンを押してください。</p>

			<form action="" method="post">
				<div class="row">
					<div class="col-xs-6 col-xs-offset-3">
						<button type="submit" class="btn btn-primary btn-block btn-flat">
							ロックを解除する
						</button>
					</div>
				</div>
			</form>

		</div>
		<!-- /.login-box-body -->
	</div>
	<!-- /.login-box -->
{/block}
{block name='script'}
{/block}

##PHP

この解除ページは、アカウントロックがされていないユーザーに表示する必要はありませんので、トークンが有効であるかどうかを判断して、必要がない場合には、非表示にしたいところです。

そこで、このページのURLは、以下のように表現しておきましょう。

URL
http://www.example.com/unlock.php?token=(解除キー)

トークンキーが一致しているユーザーからモデルを取得すれば、ユーザーを特定できます。逆にできなければ、このページを非表示にします。

基本的には、Controllerに記述します。

追加

classes/controller/LoginController.class.php
	/**
	 * トークンからユーザーモデルを取得し、ロック中かどうかを判定する
	 * @return boolean
	 */
	public static function isAccountLock()
	{
		$token = filter_input(INPUT_GET, 'token');
		$objUserModel = new UserModel();
		$objUserModel->getModelByToken($token);
		return $objUserModel->isAccountLock();
	}

追加

classes/model/UserModel.class.php
	/**
	 * トークンからユーザーを検索する
	 * @param string $token
	 * @return \MyApp\model\UserModel
	 */
	public function getModelByToken($token)
	{
		$dao = UserDao::getDaoFromToken($token);
		return (isset($dao[0])) ? $this->setProperty(reset($dao)) : null;
	}

追加

classes/dao/UserDao.class.php
	/**
	 * トークンから配列を取得する
	 * @param type $token
	 * @return array
	 */
	public static function getDaoFromToken($token)
	{
		$sql = "SELECT ";
		$sql .= "`userId`";
		$sql .= ", `password`";
		$sql .= ", `displayName`";
		$sql .= ", `email`";
		$sql .= ", `token`";
		$sql .= ", `loginFailureCount`";
		$sql .= ", `loginFailureDatetime`";
		$sql .= ", `deleteFlag` ";
		$sql .= "FROM `tbl_user` ";
		$sql .= "WHERE `token` = :token ";
		$sql .= "AND `deleteFlag` = 0 ";

		$arr = array();
		$arr[':token'] = $token;
		return Db::select($sql, $arr);
	}
webroot/unlock.php
<?php

/**
 * unlock.php
 */

namespace MyApp;

use MyApp\controller\LoginController;
use MyApp\common\Template;

define('LAYOUT', 'index');

try {
	require_once '../common.php';
	
	// アカウントロック判定
	Template::assign('is_lock', LoginController::isAccountLock());
} catch (\Exception $e) {
	Template::exception($e);
} finally {
	Template::display();
}

is_lock を使ってページの表示を切り替えます。
テンプレートを以下のように変更します。

smarty/template/unlock.tpl
{block name='meta'}
	<style type="text/css">
		.login-box, .register-box {
			margin: 20px auto;
		}
	</style>
{/block}
{block name='content'}
	<div class="login-box">
		<div class="login-logo">
			<a href="/">
				<b>Admin</b>
				LTE
			</a>
		</div>
		<!-- /.login-logo -->
		<div class="login-box-body">

			{if $is_lock}
				<p class="login-box-msg">ロックを解除するには、下のボタンを押してください。</p>

				<form action="" method="post">
					<div class="row">
						<div class="col-xs-6 col-xs-offset-3">
							<button type="submit" class="btn btn-primary btn-block btn-flat">
								ロックを解除する
							</button>
						</div>
					</div>
				</form>
			{else}
				<p class="login-box-msg">アカウントはロックされていません。</p>
				<p class="login-box-msg">
					<a href="/">ログインページ</a> よりログインしてください。
				</p>
			{/if}

		</div>
		<!-- /.login-box-body -->
	</div>
	<!-- /.login-box -->
{/block}
{block name='script'}
{/block}

##ロック解除・メソッドの実装

これも入り口となるメソッドはコントローラーに記述します。

unlock() という名前にしておきましょう。この関数が true を返したときに、テンプレートには「アカウントロックを解除しました」とメッセージを表示します。

classes/controller/UserController.class.php
	/**
	 * ロックを解除する
	 * @return boolean | null
	 */
	public static function unlock()
	{
		if (null == filter_input_array(INPUT_POST)) {
			return;
		}

		$token = filter_input(INPUT_GET, 'token');

		Db::transaction();
		$objUserModel = new UserModel();
		$objUserModel->getModelByToken($token);
		$objUserModel->setLoginFailureCount(0)
			->setLoginFailureDatetime(NULL)
			->setToken('')
			->save();
		DB::commit();

		return true;
	}
webroot/unlock.php
<?php

/**
 * unlock.php
 */

namespace MyApp;

use MyApp\controller\LoginController;
use MyApp\common\Template;

define('LAYOUT', 'index');

try {
	require_once '../common.php';
	Template::assign('is_lock', LoginController::isAccountLock());
	Template::assign('success', LoginController::unlock()); // 追加
} catch (\Exception $e) {
	Template::exception($e);
} finally {
	Template::display();
}
smarty/template/unlock.tpl
{block name='meta'}
	<style type="text/css">
		.login-box, .register-box {
			margin: 20px auto;
		}
	</style>
{/block}
{block name='content'}
	<div class="login-box">
		<div class="login-logo">
			<a href="/">
				<b>Admin</b>
				LTE
			</a>
		</div>
		<!-- /.login-logo -->
		<div class="login-box-body">

			{if $success|default:null}
				<p class="login-box-msg">アカウントロックを解除しました。</p>
				<p class="login-box-msg">
					<a href="/">ログインページ</a> よりログインしてください。
				</p>
			{else}

				{if $is_lock}
					<p class="login-box-msg">ロックを解除するには、下のボタンを押してください。</p>

					<form action="" method="post">
						<div class="row">
							<div class="col-xs-6 col-xs-offset-3">
								<button type="submit" class="btn btn-primary btn-block btn-flat">
									ロックを解除する
								</button>
							</div>
						</div>
					</form>
				{else}
					<p class="login-box-msg">アカウントはロックされていません。</p>
					<p class="login-box-msg">
						<a href="/">ログインページ</a> よりログインしてください。
					</p>
				{/if}
			{/if}

		</div>
		<!-- /.login-box-body -->
	</div>
	<!-- /.login-box -->
{/block}
{block name='script'}
{/block}

ひと通りの機能で中心的な部分を実装したことになりますが、これだけでは不十分です。このままではまだ、CSRFの問題があります。
これいついては、まだ続きで実装してゆきましょう。

12
14
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
12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?