PHPとMySQLで新規登録とログインを実装する(PDO使用)

PHPのPDOの書き方で新規登録とログインを実装してみました。個人的なメモとして記述をしておりますが、参考になればと思います。
<br>
さて、PHPには、MySQLへの接続用のAPIが mysql、mysqli、PDO の三種類があります。そのうちの mysql は PHP 5.5.0 で非推奨となり、PHP 7 で削除されたため現在は mysqli か PDO_MySQL で記述するのが主流とのことで（詳しくは[こちら](http://php.net/manual/ja/mysqlinfo.api.choosing.php)）今回は PDO を使用して実装しようと思います。

# 手順
1. データベース設定
2. ログイン作成
3. メイン作成
4. ログアウト作成
5. 新規登録作成

<br>

# （１）データベースの設定をする

今回は、MySQL管理ツールのphpMyAdminでテーブル作成やデータベース作成などを行います。

## phpMyAdminで行う手順

1. データベース作成
2. ユーザーの作成
3. テーブルの作成

これらを以下の通りに設定します。

### データベース
loginManagement

### ユーザー
- ユーザ：hogeUser
- ホスト：localhost
- パスワード：hogehoge
- グローバル権限：全てにチェック(本来はデータベースに対して必要な権限だけを持つユーザを使うようにする)

###テーブル
userData

- id （int（5）） ... primary key設定、AUTO_INCREMENT（idの被りを無くすため）
- name （varchar（20））
- password （varchar（100）） ... ハッシュ化するから少し大きめ

<br>

# （２）ログインページを作る

ログインは、**ユーザID**と**ハッシュ化されたパスワード**をデータベース内と照らし合わせて認証します。
ここでパスワードのハッシュ化についてですが、php 5.5.0 以降の関数（password_verfy()やpassword_hash()）を使用します。しかし、下記のリンクからpassword_compactをダウンロードし、同じ階層のファイルに入れて、password.phpをコード内で呼び出すだけで使用することができます。
[PHP5.3.7以降なら同名のユーザー関数が使えるようにする「password_compact」](https://github.com/ircmaxell/password_compat)
詳しい解説は[こちら](http://rei-farms.jp/blog/webmaking/2752/)

<br>

準備が整ったところでコードを書いていきます。

```html+php:Login.php
<?php
require 'password.php';   // password_verfy()はphp 5.5.0以降の関数のため、バージョンが古くて使えない場合に使用
// セッション開始
session_start();

$db['host'] = "localhost";  // DBサーバのURL
$db['user'] = "hogeUser";  // ユーザー名
$db['pass'] = "hogehoge";  // ユーザー名のパスワード
$db['dbname'] = "loginManagement";  // データベース名

// エラーメッセージの初期化
$errorMessage = "";

// ログインボタンが押された場合
if (isset($_POST["login"])) {
	// 1. ユーザIDの入力チェック
	if (empty($_POST["userid"])) {  // emptyは値が空のとき
		$errorMessage = 'ユーザーIDが未入力です。';
	} else if (empty($_POST["password"])) {
		$errorMessage = 'パスワードが未入力です。';
	}

	if (!empty($_POST["userid"]) && !empty($_POST["password"])) {
		// 入力したユーザIDを格納
		$userid = $_POST["userid"];

		// 2. ユーザIDとパスワードが入力されていたら認証する
		$dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);

		// 3. エラー処理
		try {
			$pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION));

			$stmt = $pdo->prepare('SELECT * FROM userData WHERE name = ?');
			$stmt->execute(array($userid));

			$password = $_POST["password"];

			if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
				if (password_verify($password, $row['password'])) {
					session_regenerate_id(true);

					// 入力したIDのユーザー名を取得
					$id = $row['id'];
					$sql = "SELECT * FROM userData WHERE id = $id";  //入力したIDからユーザー名を取得
					$stmt = $pdo->query($sql);
					foreach ($stmt as $row) {
						$row['name'];  // ユーザー名
					}
					$_SESSION["NAME"] = $row['name'];
					header("Location: Main.php");  // メイン画面へ遷移
					exit();  // 処理終了
				} else {
					// 認証失敗
					$errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
				}
			} else {
				// 4. 認証成功なら、セッションIDを新規に発行する
				// 該当データなし
				$errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
			}
		} catch (PDOException $e) {
			$errorMessage = 'データベースエラー';
			//$errorMessage = $sql;
			// $e->getMessage() でエラー内容を参照可能（デバッグ時のみ表示）
			// echo $e->getMessage();
		}
	}
}
?>

<!doctype html>
<html>
	<head>
			<meta charset="UTF-8">
			<title>ログイン</title>
	</head>
	<body>
		<h1>ログイン画面</h1>
		<form id="loginForm" name="loginForm" action="" method="POST">
			<fieldset>
				<legend>ログインフォーム</legend>
				<div><font color="#ff0000"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></font></div>
				<label for="userid">ユーザーID</label><input type="text" id="userid" name="userid" placeholder="ユーザーIDを入力" value="<?php if (!empty($_POST["userid"])) {echo htmlspecialchars($_POST["userid"], ENT_QUOTES);} ?>">
				<br>
				<label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力">
				<br>
				<input type="submit" id="login" name="login" value="ログイン">
			</fieldset>
		</form>
		<br>
		<form action="SignUp.php">
			<fieldset>			
				<legend>新規登録フォーム</legend>
				<input type="submit" value="新規登録">
			</fieldset>
		</form>
	</body>
</html>
```

<br>

# （３）メインページを作る

ログアウトボタンの設置とログインページで入力したユーザIDをもとに、登録されているユーザ名を呼び出してこの画面で表示するだけのページです。作りこむ際はここがメインなので、様々なコンテンツを入れていくといいと思います。


```html+php:Main.php
<?php
session_start();

// ログイン状態チェック
if (!isset($_SESSION["NAME"])) {
	header("Location: Logout.php");
	exit;
}
?>

<!doctype html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>メイン</title>
	</head>
	<body>
		<h1>メイン画面</h1>
		<!-- ユーザーIDにHTMLタグが含まれても良いようにエスケープする -->
		<p>ようこそ<u><?php echo htmlspecialchars($_SESSION["NAME"], ENT_QUOTES); ?></u>さん</p>  <!-- ユーザー名をechoで表示 -->
		<ul>
			<li><a href="Logout.php">ログアウト</a></li>
		</ul>
	</body>
</html>
```

<br>

# （４）ログアウトページを作る

セッションをクリアして、ログイン画面へ戻るボタンを設置します。
ログアウトを行った後は、必ずセッションのクリアを行うことを忘れないようにします。

```html+php:Logout.php
<?php
session_start();

if (isset($_SESSION["NAME"])) {
	$errorMessage = "ログアウトしました。";
} else {
	$errorMessage = "セッションがタイムアウトしました。";
}

// セッションの変数のクリア
$_SESSION = array();

// セッションクリア
@session_destroy();
?>

<!doctype html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>ログアウト</title>
	</head>
	<body>
		<h1>ログアウト画面</h1>
		<div><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></div>
		<ul>
			<li><a href="Login.php">ログイン画面に戻る</a></li>
		</ul>
	</body>
</html>
```

<br>

# （５）新規登録ページを作る

この画面では、ユーザ名とパスワードを入力させて登録をします。その後、データベース側でAUTO_INCREMENTに設定してあるIDと先ほど入力したパスワードを表示させるプログラムです。（ここで表示させたIDとパスワードは、ログイン時に使用されるものです）

<br>

また、ログインページと同様に password_hash() の関数を扱うため、以下の一文を加えます。

```php
require 'password.php';
```

<br>

今回パスワードは再度入力させるようにして、入力ミスなどの防止をしました。また、エラーメッセージは赤色で表示させ、登録完了メッセージは青色で表示させるようにしました。メッセージを表示させる前に、初期化を必ず行います。

```html+php:SignUp.php
<?php
require 'password.php';   // password_hash()はphp 5.5.0以降の関数のため、バージョンが古くて使えない場合に使用
// セッション開始
session_start();

$db['host'] = "localhost";  // DBサーバのURL
$db['user'] = "hogeUser";  // ユーザー名
$db['pass'] = "hogehoge";  // ユーザー名のパスワード
$db['dbname'] = "loginManagement";  // データベース名

// エラーメッセージ、登録完了メッセージの初期化
$errorMessage = "";
$signUpMessage = "";

// ログインボタンが押された場合
if (isset($_POST["signUp"])) {
	// 1. ユーザIDの入力チェック
	if (empty($_POST["username"])) {  // 値が空のとき
		$errorMessage = 'ユーザーIDが未入力です。';
	} else if (empty($_POST["password"])) {
		$errorMessage = 'パスワードが未入力です。';
	} else if (empty($_POST["password2"])) {
		$errorMessage = 'パスワードが未入力です。';
	}

	if (!empty($_POST["username"]) && !empty($_POST["password"]) && !empty($_POST["password2"]) && $_POST["password"] === $_POST["password2"]) {
		// 入力したユーザIDとパスワードを格納
		$username = $_POST["username"];
		$password = $_POST["password"];

		// 2. ユーザIDとパスワードが入力されていたら認証する
		$dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);

		// 3. エラー処理
		try {
			$pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION));

			$stmt = $pdo->prepare("INSERT INTO userData(name, password) VALUES (?, ?)");

			$stmt->execute(array($username, password_hash($password, PASSWORD_DEFAULT)));  // パスワードのハッシュ化を行う（今回は文字列のみなのでbindValue(変数の内容が変わらない)を使用せず、直接excuteに渡しても問題ない）
			$userid = $pdo->lastinsertid();  // 登録した(DB側でauto_incrementした)IDを$useridに入れる

			$signUpMessage = '登録が完了しました。あなたの登録IDは '. $userid. ' です。パスワードは '. $password. ' です。';  // ログイン時に使用するIDとパスワード
		} catch (PDOException $e) {
			$errorMessage = 'データベースエラー';
			// $e->getMessage() でエラー内容を参照可能（デバッグ時のみ表示）
			// echo $e->getMessage();
		}
	} else if($_POST["password"] != $_POST["password2"]) {
		$errorMessage = 'パスワードに誤りがあります。';
	}
}
?>

<!doctype html>
<html>
	<head>
			<meta charset="UTF-8">
			<title>新規登録</title>
	</head>
	<body>
		<h1>新規登録画面</h1>
		<form id="loginForm" name="loginForm" action="" method="POST">
			<fieldset>
				<legend>新規登録フォーム</legend>
				<div><font color="#ff0000"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></font></div>
				<div><font color="#0000ff"><?php echo htmlspecialchars($signUpMessage, ENT_QUOTES); ?></font></div>
				<label for="username">ユーザー名</label><input type="text" id="username" name="username" placeholder="ユーザー名を入力" value="<?php if (!empty($_POST["username"])) {echo htmlspecialchars($_POST["username"], ENT_QUOTES);} ?>">
				<br>
				<label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力">
				<br>
				<label for="password2">パスワード(確認用)</label><input type="password" id="password2" name="password2" value="" placeholder="再度パスワードを入力">
				<br>
				<input type="submit" id="signUp" name="signUp" value="新規登録">
			</fieldset>
		</form>
		<br>
		<form action="Login.php">
			<input type="submit" value="戻る">
		</form>
	</body>
</html>
```

<br>

# 参考にしたリンク先一覧

- [HTTPリファレンス](http://www.tohoho-web.com/html/index.htm)
<br>

- [MySQLの基礎](http://www.php-labo.net/tutorial/mysql/)
<br>

- [MySQLの基本的な操作](http://www.php-labo.net/tutorial/mysql/operate.html)
<br>

- [セッション管理](http://www.phpbook.jp/tutorial/session/)
<br>

- [【初心者向け】PHP5とMySQLでつくるログイン機能のサンプルアプリケーション](http://replication.hatenablog.com/entry/2014/06/30/005815)
<br>

- [PHP5.5から使える暗号化「password_hash」を「password_compact」を使って以前のバージョンで利用する](http://rei-farms.jp/blog/webmaking/2752/)
<br>

- [PHP : どの API を使うか](http://php.net/manual/ja/mysqlinfo.api.choosing.php)
<br>

- [PHPとMySQLのSELECT文でデータ取得(PDO)](http://www.flatflag.nir87.com/select-932)
<br>

- [PHPでPDOを使ってMySQLに接続、INSERT、UPDATE、DELETE、COUNT、SUM](http://qiita.com/tabo_purify/items/2575a58c54e43cd59630)
<br>

<br>

# 最後に
今回はPDOの書き方で新規登録やログインを作りましたが、もっと効率の良い方法はあると思います。しかし、少しでも誰かのお役に立てればと思います。私自身まだまだPHPなどの経験が浅いためもっと勉強しなければと記事を書いていて改めて感じました。コードなどで間違えている箇所がございましたらコメント頂けると嬉しいです。
[こちら](https://github.com/KosukeGit/loginManagement)にもソースコードを載せておきます。

<br>

更新しました（2017/1/31）

- ユーザ名取得時の書き方を一部変更
- 変数名変更
- パスワードのチェックの際の比較演算子を修正
- XSSの危険性があるため、その修正