【PHP】RPGゲームを作る4(ユーザ登録画面)

PHP

ユーザの新規登録画面を作成しました。登録時にはメールアドレスの有効性を確認する為に、6桁のPINコードを送信し、このPINコードを入力したユーザだけを正規ユーザとみなす仕組みです。

環境

 OS ubuntu22.04 
 Webサーバ apache2.4.52
   ※メール送信の為にsendmailパッケージを有効にしています
(自身のIP 192.168.19.128、公開フォルダ /var/www/html/php/rpg)
 PHP 8.1.2-1ubuntu2.14
 DB MySQL Ver 8.0.34-ubuntu0.22.04.1

ユーザデータ保管用のテーブル作成

ubuntu上で次のSQLを実行し、ユーザ管理用のテーブルを作成しました。

$ > mysql -u ******** -p
パスワードを入力
mysql > user rpg
mysql > create table user_data (
        user_no int PRIMARY KEY,username varchar(30) ,password varchar(255),
        mailaddress varchar(255),
lastmodiffy timestamp
,now_status varchar(20),
	now_map int,now_x int,now_y int,lv int,exp int,now_hp int,now_mp int,
        max_hp int,max_mp int,str int,def int,status varchar(30),pin varchar(10),
        status varchar(30),pin varchar(10));

とりあえずこれくらいのステータスがあればいいかと思い見切りで作成しました。
 足りなければAlter tableすればいいかと思います。
 仮登録ユーザと正規登録ユーザの違いは、statusが
  _INTERRIN_ : 仮登録ユーザ(PINを入力していないユーザ)
_REGEST_ :正規登録ユーザ(PINを入力したユーザ)
としました。

ユーザ登録画面の作成

次のPHPプログラムを作成し、アップロードしてからパーミッション705にしました。

<?PHP
//定数

// MySQL接続情報
const SERVER_NAME = '192.168.19.128'; // データベースのホスト名
const DB_USER = '********'; // データベースのユーザー名
const DB_PASSWORD = '********'; // データベースのパスワード
const DB_NAME = 'rpg'; // 使用するデータベース名

//mail
const MAIL_FROM ='********@********.***.ne.jp';

const DEF_POST = 'POST';
const DEF_CREATE = '_CREATE_';
const DEF_DO = 'do';
const DEF_USERNAME = 'user_name';
const DEF_USERPASS = 'user_pass';
const DEF_MAIL = 'user_mail';
const DEF_PIN = 'user_pin';
const DEF_STATUS_CREATE ='CREATE';
const DEF_STATUS_CREATE2 ='PUSH_PIN';
const DEF_STATUS_INTERRIM ='_INTERRIM_';
const DEF_STATUS_REGEST ='_REGEST_';

?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<?PHP

//db読込
// MySQLサーバーへの接続
$conn = new mysqli(SERVER_NAME, DB_USER, DB_PASSWORD, DB_NAME);

// 接続エラーの確認
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

//ヘッダー
header_page($conn);

//メイン処理
if ($_SERVER['REQUEST_METHOD'] === DEF_POST 
	&& isset($_POST[DEF_DO])
	&& $_POST[DEF_DO] === DEF_CREATE
	&& isset($_POST[DEF_USERNAME])
	&& isset($_POST[DEF_USERPASS])
	&& isset($_POST[DEF_MAIL])){
	
	//仮登録画面(ユーザ名の一意チェック、DB登録、PIN生成、PINメール送信、PIN入力待ち受けまで)
	kari_regest_page($conn,$_POST);
	
}elseif($_SERVER['REQUEST_METHOD'] === DEF_POST 
	&& isset($_POST[DEF_DO])
	&& $_POST[DEF_DO] === DEF_STATUS_CREATE2
	&& isset($_POST[DEF_USERNAME])
	&& isset($_POST[DEF_USERPASS])
	&& isset($_POST[DEF_MAIL])
	&& isset($_POST[DEF_PIN])){
	
	//本登録(ユーザ名とPINの整合性チェック、有効なPINならステータスを更新しログイン画面、無効なPINならPIN待ち受け画面)
	honn_regest_page($conn,$_POST);

}else {
	//ユーザ名待ち受け画面
	new_page($conn);

}

//フッター
footer_page($conn);
$conn->close();

?>
</body>
</html>
<?PHP //PHP function  ****************************************************************
// header page************************************************************************
function header_page($conn){
	echo "<h1>ユーザ登録画面</h1>";
}

// main page**************************************************************************
function new_page(){
?>
<script type="text/javascript">
<!--
function create_user(){
	if(	document.getElementById("<?PHP echo DEF_USERNAME; ?>").value === ""){
		alert("ユーザ名を入力してください");
		exit;
	}
	if(	document.getElementById("<?PHP echo DEF_USERPASS; ?>").value === ""){
		alert("パスワードを入力してください");
		exit;
	}
	if(	document.getElementById("<?PHP echo DEF_MAIL; ?>").value === ""){
		alert("メールアドレスを入力してください");
		exit;
	}
	document.getElementById("<?PHP echo DEF_DO; ?>").value ="<?PHP echo DEF_CREATE; ?>";
	document.getElementById("create_user_button").value="登録中...しばらくお待ちください。";
	document.getElementById("create_user_button").disabled  =true;
	document.myform.submit();
}
//-->
</script>
<?PHP
	echo '<form action="" name="myform" method="'.DEF_POST.'">';
	echo '<input type="hidden" name="'.DEF_DO.'" id="'.DEF_DO.'" value="" >';
	echo 'ユーザ名<input type="text" name="'.DEF_USERNAME.'" id="'.DEF_USERNAME.'" value="" ><br>';
	echo 'パスワード<input type="text" name="'.DEF_USERPASS.'" id="'.DEF_USERPASS.'" value=""><br>';
	echo 'メールアドレス<input type="text" name="'.DEF_MAIL.'" id="'.DEF_MAIL.'" value="">※登録確認メールを送信します。<br>';
	echo '<input type="button" value="登録" id="create_user_button" onclick="create_user();"><br>';
	echo '</FORM>';
}
//パスワードジェネレータ****************************************************************
function pass_gene($user_name,$password){
	$pepper = "kosyou_";
	$gene_pass = hash('sha256', $user_name . $pepper . $password);
	return $gene_pass;
}

//仮登録画面  **************************************************************************
function kari_regest_page($conn,$PO){
	//仮登録とメール送信
	//$user_name = $PO[DEF_USERNAME];//2023-11-14 修正
        $user_name = htmlspecialchars($PO[DEF_USERNAME], ENT_QUOTES, 'UTF-8');
	// パスワードを生成する
	$hashed_password = pass_gene($user_name, $PO[DEF_USERPASS]);
	$user_mail = $PO[DEF_MAIL]; //mailaddress
	
	//仮登録のまま24時間が経過したデータはここで削除する
	// 24時間前の日時を取得
	$twentyFourHoursAgo = date('Y-m-d H:i:s', strtotime('-24 hours'));
	// データの削除
	$sql = "DELETE FROM user_data WHERE lastmodiffy < '$twentyFourHoursAgo' and status ='".DEF_STATUS_INTERRIM."' ";
	if ($conn->query($sql) === TRUE) {
	    //echo "レコードが削除されました";
	} else {
	    //echo "エラー: " . $conn->error;
	}
	
	
	//登録前確認
	$stmt = $conn->prepare("SELECT * FROM user_data WHERE username=?");
	$stmt->bind_param("s", $user_name);
	$stmt->execute();
	$result = $stmt->get_result();
	if ($result->num_rows > 0) {
		echo "同じ名前のユーザがすでに登録されています";
		//新規登録画面の表示
		new_page($conn);

	}else {
		//仮登録処理
		$sql = "SELECT MAX(user_no) as max_no FROM user_data";
		$result = $conn->query($sql);
		$row = $result->fetch_assoc();
		$user_no = $row["max_no"] + 1;
		$lastmodify = date('Y-m-d H:i:s');
		
		//初期値の設定
		$now_status = DEF_STATUS_CREATE;
		$now_map = 1;
		$now_x = 3;
		$now_y = 3;
		$lv = 1;
		$exp = 0;
		$now_HP=50;
		$now_MP=25;
		$max_HP=50;
		$max_MP=25;
		$STR=5;
		$DEF=5;
		$status=DEF_STATUS_INTERRIM;
		
		// ランダムな6桁のPINを生成
		$pin = strval(mt_rand(100000, 999999));
		
		$sql = "insert into user_data(user_no,username,password,mailaddress,lastmodiffy
						,now_status,now_map,now_x,now_y,LV
						,EXP,now_HP,now_MP,max_HP,max_MP
						,STR,DEF,status,pin) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
		$stmt = $conn->prepare($sql);
		$stmt->bind_param("isssssiiiiiiiiiiiss", $user_no ,$user_name,$hashed_password,$user_mail,$lastmodify
						,$now_status,$now_map,$now_x,$now_y,$lv
						,$exp,$now_HP,$now_MP,$max_HP,$max_MP
						,$STR,$DEF,$status,$pin);
		if ($stmt->execute()) {
    		echo "仮登録が完了しました。<br>";
    		
    		//メール本文作成
			$subject = "たんすの狭間RPG ユーザ登録確認メール";
			$message = "たんすの狭間RPGへようこそ。".PHP_EOL.
				"登録を完了するためには次のPINコードをユーザ登録画面に入力してください。".PHP_EOL.PHP_EOL.
				"PIN :".$pin.PHP_EOL.PHP_EOL.
				"メールに心当たりがない方はメールを削除願います。".PHP_EOL.
				"このメールは返信できません。";

			// Additional headers
			$headers = "From: ".MAIL_FROM . PHP_EOL . 
	    	"Reply-To: ".MAIL_FROM . PHP_EOL .
	    	"X-Mailer: PHP/" . phpversion();
	
			// Send emailでメール送信(sendmail モジュールが必要)
			if (mail($user_mail, $subject, $message, $headers)) {
			    //pin待ち受け画面の表示
			    echo "登録に必要なPINを記載したメールを送信しました。<br>";
			    pinwait_page($conn,$PO,$hashed_password);

			} else {
				//メールの送信に失敗した場合
    			echo "メールの送信に失敗しました。管理者に連絡願います。";
			}
		}
	}
	//$connは再利用するので閉じない
	$stmt->close();
}

//本登録画面  **************************************************************************
function honn_regest_page($conn,$PO){
        //$user_name = $PO[DEF_USERNAME];//2023-11-14 修正
        $user_name = htmlspecialchars($PO[DEF_USERNAME], ENT_QUOTES, 'UTF-8');
	$hashed_password = $PO[DEF_USERPASS];//すでにhash化されている
	$user_mail = $PO[DEF_MAIL];
	$user_pin =  $PO[DEF_PIN];

	$stmt = $conn->prepare("SELECT pin FROM user_data WHERE username=? and password=?");
	$stmt->bind_param("ss", $user_name,$hashed_password);
	$stmt->execute();
	$result = $stmt->get_result();
	$row = $result->fetch_assoc();
	
	if( isset($user_pin) && isset($user_name) && $row["pin"] === $user_pin ){
		echo "PINを確認しました。<br>";
		//登録
		//uodate statusを変更、ログイン画面を表示
		$stmt = $conn->prepare("UPDATE user_data  SET status=? WHERE username=? and password=?");
		$str= DEF_STATUS_REGEST;
		$stmt->bind_param("sss", $str,$user_name,$hashed_password);
		if($stmt->execute()){
			echo "正規登録完了";
		} else{
			echo "登録失敗" . $str;
		}
		
		//ログイン画面
		echo "ようこそ" . $user_name."さん。<br>";
		echo '<form action="./rpg.php" name="myform" method="'.DEF_POST.'">';
		echo '<input type="hidden" name="'.DEF_USERNAME.'" id="'.DEF_USERNAME.'" value="'.$user_name.'" >'.$user_name.'<br>';
		echo '<input type="hidden" name="'.DEF_USERPASS.'" id="'.DEF_USERPASS.'" value="'.$hashed_password .'">';
		echo '<input type="submit" value="入口" id="push_start_button" "><br>';
		echo '</FORM>';
	} else {
		echo "PINが一致しません。<br>";
		echo "メールが受信できない場合は、<br>";
		echo "・迷惑メールボックスを確認する<br>";
		echo "・受信拒否設定を確認し「".MAIL_FROM."」ドメインからの受信を許可する<br>";
		echo "を確認してください。<br>";
		
		//pin待ち受け画面の表示
		pinwait_page($conn,$PO,$hashed_password);
		
	}
	//$connは再利用するので閉じない
	$stmt->close();
}

// PIN待ち受けページ********************************************************************
function pinwait_page($conn,$PO,$hashed_password){
	$user_name = $PO[DEF_USERNAME];
	$user_mail = $PO[DEF_MAIL]; //mailaddress
	echo "PINを入力し、ユーザ登録を完了させてください。";
?>
<script type="text/javascript">
<!--
function push_pin(){
	if(	document.getElementById("<?PHP echo DEF_PIN; ?>").value === ""){
		alert("PINを入力してください");
		exit;
	}
	document.getElementById("<?PHP echo DEF_DO; ?>").value ="<?PHP echo DEF_STATUS_CREATE2; ?>";
	document.getElementById("push_pin_button").value="登録中...しばらくお待ちください。";
	document.getElementById("push_pin_button").disabled  =true;
	document.myform.submit();
}
//-->
</script>
<?PHP
	echo '<form action="" name="myform" method="'.DEF_POST.'">';
	echo '<input type="hidden" name="'.DEF_DO.'" id="'.DEF_DO.'" value="'.DEF_STATUS_CREATE2.'" >';
	echo 'ユーザ名:<input type="hidden" name="'.DEF_USERNAME.'" id="'.DEF_USERNAME.'" value="'.$user_name.'" >'.$user_name.'<br>';
	echo 'パスワード:<input type="hidden" name="'.DEF_USERPASS.'" id="'.DEF_USERPASS.'" value="'.$hashed_password .'">暗号化して保存しています<br>';
	echo 'メールアドレス:<input type="hidden" name="'.DEF_MAIL.'" id="'.DEF_MAIL.'" value="'.$user_mail.'">'.$user_mail.'<br>';
	echo 'PIN:<input type="text" name="'.DEF_PIN.'" id="'.DEF_PIN.'" value=""><br>';
	echo '<input type="button" value="登録" id="push_pin_button" onclick="push_pin();"><br>';
	echo '</FORM>';

}
// footer page**************************************************************************
function footer_page($conn){
	echo '<hr><A href="./user_create.php">reload</a><br>';
}

?>

パスワードの管理については、ユーザが登録したパスワードは、DBに登録しないで、ハッシュ値を保存するようにしています。
この際にハッシュ値のレインボーテーブル対策として、pepperとハッシュ値の平均化を防ぐための対策をとっています。
 この作りだと、登録中に画面のリロードをするなどして正規登録終了まで行けなかった場合に仮ユーザ情報が残ってしまうので、仮登録ユーザ情報は、24時間以内にPINコードを入力しないと削除されるようにしています。
 本来はシェル等で定期的に削除プログラムを実行するべきですが、ユーザの登録確認時に裏でコッソリ仮データを削除しています。

プログラムを実行した様子

ユーザIDやパスワードを入力する画面

登録後の画面、PINを入力すると正規登録となります

受信したメールはこんな感じです。

登録確認メールはこんな感じです