ユーザの新規登録画面を作成しました。登録時にはメールアドレスの有効性を確認する為に、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を入力すると正規登録となります
受信したメールはこんな感じです。
登録確認メールはこんな感じです