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


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. アカウントロック解除機能の実装
11. [CSRF対策](http://qiita.com/ShibuyaKosuke/items/8100e304e4447e44e98c)

前回の続きです。

##はじめに

要件をまとめます。

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

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

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

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

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

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

			return $this->save();
		}

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


##HTMLの作成

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

```html: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は、以下のように表現しておきましょう。

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

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

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

追加

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

追加

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

追加

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

```php: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` を使ってページの表示を切り替えます。
テンプレートを以下のように変更します。

```html: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` を返したときに、テンプレートには「アカウントロックを解除しました」とメッセージを表示します。

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

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

```html: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`の問題があります。
これいついては、まだ続きで実装してゆきましょう。