Posted at

PHP For Beginnersチュートリアル その12 基本的な会員登録及びログインフォームを作る


このシリーズの目的

体系的なwebコーディングの訓練ができるようになるためにPHPの初学のきっかけかつ、PHPでログインフォームやフォームを実装することができるようになるために

PHP For Beginners

上記のチュートリアルを進めているのでその備忘録。

前回


内容

今回のチュートリアル

PHP Email Verification And Validation Tutorial - Registration & Login Form

このチュートリアルでやること

・簡単な会員登録フォームを作る

・会員登録フォームから情報を受け取ったら、

 入力されたメールアドレスに認証リンクが記載されたメールを送る

・簡単なログインフォームを作る

成果物


register.php


<?php

$msg = "";
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

if (isset($_POST['submit'])) {

$con = new mysqli("localhost","root","","register");

$name = $con->real_escape_string($_POST['username']);
$email = $con->real_escape_string($_POST['email']);
$password = $con->real_escape_string($_POST['password']);
$cPassword = $con->real_escape_string($_POST['cPassword']);

if ($name == "" || $email == "" || $password != $cPassword)
$msg = "Please check your inputs!";
else {
$sql = $con->query("SELECT id FROM users WHERE email = '$email' ");
if ($sql->num_rows > 0) {
$msg = "Email already exists in the database!";
} else {
$token = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789!$/()*';
$token = str_shuffle($token);
$token = substr($token, 0, 10);

$hashedPassword = password_hash($password, PASSWORD_BCRYPT);

$con->query("INSERT INTO users (name,email,password,isEmailConfirmed,token)
VALUES('
$name', '$email', '$hashedPassword', '0', '$token');
"
);

mb_language("japanese");
mb_internal_encoding("UTF-8");
require 'vendor/autoload.php';
require 'Mairtrap-config.php';

$mail = new PHPMailer();

// Server

$mail->SMTPDebug = 0; //本番では0とかにする。
$mail->isSMTP();
$mail->SMTPAuth = true;
$mail->Host = MAIL_HOST;
$mail->Username = MAIL_USERNAME;
$mail->Password = MAIL_PASSWORD;
$mail->SMTPSecure = MAIL_ENCRPT;
$mail->Port = SMTP_PORT;

// Recipients
$mail->setFrom(FROM_MAIL);
$toname = mb_encode_mimeheader("$name", 'ISO-2022-JP', 'B', "\n");
$mail->addAddress($email,$toname);
// $mail->addAttachment($attachment);

// Content
$mail->Subject = mb_encode_mimeheader("Please verify email!", "ISO-2022-JP", "UTF-8");
$mail->Body = mb_convert_encoding("
Please cilck on the link below:<br><br>

<a href='http://localhost/Laravel/PHPMailer/Training/phptutorial13/confirm.php?email=$email&token=$token'>Click here</a>
"
,"JIS","UTF-8");
$mail->CharSet = 'ISO-2022-JP';
$mail->Encoding = "7bit";

// Select HTML or NOT

$mail->isHTML(true);

if ($mail->send()) {
$msg = "You have been registered! Please verify your email!";
} else {
$msg = "Something wrong happend! Please try again!";
}

}
}
}

?>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Registration & Login Form</title>
<link rel="stylesheet" type="text/css" href="css/register.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-md-offset-4">

<?php
if ($msg != "")
echo $msg . "<br><br>";

?>

<form action="register.php" method="post" accept-charset="utf-8">
<input class="form-control" type="text" name="username" placeholder="Name......"><br>
<input class="form-control" type="text" name="email" placeholder="Email......"><br>
<input class="form-control" type="password" name="password" placeholder="Password......"><br>
<input class="form-control" type="password" name="cPassword" placeholder="Confirm Password......"><br>
<input class="btn btn-primary" type="submit" name="submit" value="Register">
</form>
<a href="http://localhost/Laravel/PHPMailer/Training/phptutorial13/login.php">Click here to login</a>
</div>

</div>

</div>
</body>
</html>



confirm.php


<?php

function redirect() {
header('location: register.php');
exit();

}

if (!isset($_GET['email']) || !isset($_GET['token'])) {

redirect();

} else {

$con = new mysqli("localhost","root","","register");

$email = $con->real_escape_string ($_GET['email']);
$token = $con->real_escape_string ($_GET['token']);

$sql = $con->query("SELECT id FROM users WHERE email='$email' AND token='$token' AND isEmailConfirmed=0");

if($sql->num_rows > 0) {

$con->query("UPDATE users SET isEmailConfirmed=1, token='' WHERE email='$email' ");

echo "Your email has been verified! You can log in now!";

} else {

redirect();

}

}

?>



login.php


<?php

$msg = "";

if (isset($_POST['submit'])) {

$con = new mysqli("localhost","root","","register");

$email = $con->real_escape_string($_POST['email']);
$password = $con->real_escape_string($_POST['password']);

if ($email == "" || $password == "")
$msg = "Please check your inputs!";
else {
$sql = $con->query("SELECT id, password, isEmailConfirmed FROM users WHERE email = '$email' ");

if ($sql->num_rows > 0) {

$data = $sql->fetch_array();

if(password_verify($password,$data['password'])) {

if($data['isEmailConfirmed'] == 0)
$msg= "Please verify your Email!";

else {

$msg = "You have been logged in!";

}

} else
$msg = "Please check your Password!";

} else {
$msg = "Please check your Email!";

}
}
}

?>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Registration & Login Form</title>
<link rel="stylesheet" type="text/css" href="css/register.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-md-offset-4">

<?php
if ($msg != "")
echo $msg . "<br><br>";

?>

<form action="login.php" method="post" accept-charset="utf-8">
<input class="form-control" type="text" name="email" placeholder="Email......"><br>
<input class="form-control" type="password" name="password" placeholder="Password......"><br>
<input class="btn btn-primary" type="submit" name="submit" value="Login">
</form>
<a href="http://localhost/Laravel/PHPMailer/Training/phptutorial13/register.php">You are not register yet? Please click here and register now.</a>

</div>

</div>

</div>
</body>
</html>



register.css

.container {

margin-top:100px;

}

.row {
text-align: center;
}



動作

https://youtu.be/oHF7gYZt0RA


手順

・大枠のアルゴリズムを考える

1.登録フォームに必要な情報を入力させる

→名前・Email・パスワード(確認のために2回入力させる)

2.登録フォームから送信された値をデータベースに登録し、同時にパスワードをハッシュ化して、トークンを発行する

3.認証リンクが記載されたEmailをフォームに入力されたメールアドレス宛に送信する

4.ユーザーが認証リンクをクリックするとトークンが消費され、メールアドレスの認証が行われ、ログインができるようになる

5.最後にログインフォームに入力されたEmail及びパスワードがデータベースに登録されたものと相違ないかチェックを行い、問題なければログイン成功メッセージを表示する

・各段階でのアルゴリズムを考える

*データベースの接続などは割愛。

1.登録フォーム



・フォームに入力された文字列をエスケープする

・フォームのどこかしらが空入力、及びパスワードが2回正しく入力されていない場合にはエラーを出す

・テーブルに登録されているデータのうち、メールアドレスを検索フィールドにしてidの値を返す、返せた場合はすでにそのメールアドレスが登録されている旨のエラーメッセージを表示し、返せなかった場合はランダムトークンを発行し、入力されたパスワードをハッシュ化した後、テーブルにフォームから受けとった値を新たに登録する。

・PHPMaillerなどを用いて、テーブルに登録されたユーザー宛に認証リンクが記載されたメールを送信する。

2.認証処理



・メールアドレスとtokenを取得できなかった場合

(認証リンクが記載されたメールの送信先のメールアドレスからリンクが踏まれなかった場合)、登録フォームにリダイレクトし、取得できた場合は取得した値をエスケープし、それを元にテーブルの検索フィールドをメールアドレス、トークン、及びメールアドレスが認証済みか否かで設定して、idの値を検索する。

・検索した後、idの値が返ってきた場合はメールアドレス認証済みにデータベースを変更し、トークンを消費した後、ユーザー側には認証完了とログインを促すメッセージを表示する。

返らなかった(認証済み)のであれば、登録フォームにリダイレクトする。

3.ログインフォーム



・フォームに入力された値をエスケープする

・メールアドレスまたはパスワード、あるいはどちらも未入力の場合はエラーメッセージを出し、問題なく入力されていた場合はデータベースに接続して、メールアドレスを検索フィールドにしてid・パスワード・メールアドレス認証済みか否か(0か1)の値を返す

・値が返ってきたらそれらを取得し、まずメールアドレス認証が済んでいるか否かでログイン成功判定をする、次にそれをクリアした場合は入力されたメールアドレス及びパスワードがテーブルに登録されているものと相違ないかでログイン成功判定をする。

*今回の場合はメール認証及びパスワードの正誤で条件分岐を作り、そのどちらもクリアしているがエラーになる=メールアドレスが間違っているというアルゴリズムでメールアドレスの正誤判定を行っている。


今回のコードの注釈


type=password


<input class="form-control" type="password" name="password" placeholder="Password......"><br>
<input class="form-control" type="password" name="cPassword" placeholder="Confirm Password......"><br>


inputタグのtype属性をpasswordにすると入力された文字列が*で表示される。


トークンについて


$token = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789!$/()*';
$token = str_shuffle($token);
$token = substr($token, 0, 10);


str_shuffle関数は任意の文字列をシャッフルし、考えられるすべての順列のうちから一つを返す関数。

さらに、それを変数、開始位置、終了位置を引数に設定することである文字列から文字列を抜き出して返すsubstr関数を用いて、今回はトークンを作成する。

今回のトークンは10文字の文字列となる。

トークンは その2 で触れたようにCSRF対策のために用いるものである。

トークンの作り方はその2にあるような作り方や固定トークン、そして今回のような1番簡単な方法等様々あるので、業務に携わる場合はセキュリティに応じて最適なトークンの作り方ができるように知見を広げておきたい。


password_hashとpassword_verifyについて


$hashedPassword = password_hash($password, PASSWORD_BCRYPT);

if(password_verify($password,$data['password'])) {
}


password_hashは指定したアルゴリズムを用いてパスワードをハッシュ化する関数である。

アルゴリズムはハッシュ化したいパスワードが代入された変数の次に指定してやる。

今回はBCRYPTというアルゴリズムである。

DEFAULTを指定するとPHPのバージョンによって指定するされているデフォルトのアルゴリズムが用いられる。

デフォルトのアルゴリズムが変更された場合はそれに応じて使用されるアルゴリズムも変更される。

ハッシュ化とはということと暗号化との違いはその2で扱ったのでそちらを参照。

password_verifyは保存されたハッシュの比較に使う関数で指定したハッシュがパスワードにマッチするかどうかを調べる。

参考サイトにも後記するがこちらの記事がわかりやすく説明してくれているので要参照。

2018年のパスワードハッシュ


補足 PHPMailler及び、MySQLで日本語を文字化けさせないための設定

その7 においてPHPMaillerはそのまま使うと日本語は文字化けするので、日本語を使用する際には設定を追記しなければいけないということを学習済みであるが、今回はその7のままだと受信者の名前が文字化けするのでそれに関する設定の追記と、MYSQLで日本語のデータを登録する際はそのままだと文字化けするので設定を変更しておく。


$toname = mb_encode_mimeheader("$name", 'ISO-2022-JP', 'B', "\n");
$mail->addAddress($email,$toname);

PHPMailler側の追記箇所はこの部分。

addAddressで引数に設定する前に、md_encode_mimeheaderでエンコードしておく。

ちなみに今回addAddressで指定する引数はどちらもフォームから受け取った値が代入された変数になるので、config.php等で別に設定を読み込んでいる場合はそれに応じてconfig.phpを変更するか、該当箇所をコメントアウトして今回のように引数を設定してやる。


my.ini


[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqldump]
default-character-set=utf8

[mysqld]
character-set-server=utf8


my.iniを探し出して、上記のコードを追記する。

そして、追記した後に必ずMySQLを再起動する。

再起動させないと設定は反映されない(1敗)


あとがき

今回作成したフォームは動画やコードを見ればわかるが、フォームの値が空またはパスワードが2回正しく入力できていないなどのエラー以外は適当な値でもデータベースに登録されてしまうため、それを弾くアルゴリズムを組んでいくことと今回も出てきたトークン等第二回で触れたように、webページ上の脅威やCSRFなどの攻撃に対してのセキュリティも学んでいくことが今後の私達初学者の課題である。

ログインフォーム自体は別のチュートリアルでjQueryも用いて作成するチュートリアルがあるのでそのタイミングで実装しよう。

あくまで今回は登録フォーム→メールアドレスの認証→ログインフォームという日常よくある処理がどのようなアルゴリズムで行われているのかということの基礎と基本を確認するためのチュートリアルであるので、それだけはしっかり押さえて次に進んでいきたい


参考

2018年のパスワードハッシュ

一番分かりやすい OAuth の説明

mysqlで日本語を扱えるようにする

mysqlで日本語が文字化けするときは?

password_hash

password_verify