【PHP】ログイン機能+メール送信機能をゼロから自作してみた

2023.06.02

PHPの学習を始めて1約か月ほど経ち、手持ちの教材を終わらせる段階まできた。

そこで今まで学んだことのアウトプットとして、ログイン機能と登録時のメール送信機能を自作してみたので、その記録を忘れないうちに残しておく。

大まかな処理の流れとしては下記の通りだ。

  • ①新規登録
  • index.php(トップページ)→ register_form.php(登録フォーム)→ db.php(データベース接続)+register.php(登録処理)→index.php(成功後リダイレクト)
  • ②ログイン(既存ユーザ)
  • index.php(トップページ)→ login_form.php(登録フォーム)→ db.php(データベース接続)+login.php(ログイン処理)→index.php(成功後リダイレクト)

上記の処理の流れと、各ファイルの使い方などを解説していく。

トップページ(index.php)

まずはルートとなるindex.phpだ。

既にHTMLの中にPHPが書かれているが、後の処理によってログインボタンとログアウトボタンの表示が切り替わる仕様になっているので、一旦無視でオッケー。

とりあえず登録、ログイン後はこのページにリダイレクトされることになる。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
</head>

<body>
    <div class="wrap">
        <div class=""></div>
        <div class="login">
            <?php
                include 'session.php';//ログイン前後の出し分けを要素を管理
                session_part_01($script);
            ?>
        </div>
        <p>ダミー要素</p>
        <form action=""></form>
    </div>
</body>

</html>

データベース関連(db.php)

まずはデータベースに接続するためのファイルを用意しておく。

これは後の登録処理、ログイン処理の際にもincludeで使用することになるので、単独のファイルに分けておく。

<?php
//-----データベースへの接続-----------------------------------------------------------------------------------------------------------------------------------------------------------
try {//{}の中で処理が失敗したら、catch{}の中の処理を執行
    $dsn = 'mysql:host=localhost;dbname=c_app';//localhostのc_appというテーブルに接続
    $username = 'root';//DBのユーザーネーム
    $password = '';//DBのパスワード
    $db = new PDO($dsn, $username, $password);//接続するDB指定。もし指定されたパスにデータベースファイルが存在しない場合、new PDO()の呼び出し時に自動的にファイルが作成される
} catch (PDOException $e) {//↑が失敗したら実行する処理。tryにnew PDOを使う場合はPDOExceptionを使用しておく(でないとブラウザにPHPのエラーが直接出る)
    //$eは例外オブジェクトをキャッチした際に代入される変数(任意の文字)。PDOExceptionはデータベース接続時に発生する可能性があるいくつかのエラーをキャッチするために使用(エラーの詳細な情報を取得し、適切なエラーハンドリングを行うことができる)
    echo "データベースに接続できません。" . $e->getMessage();//getMessage()は例外のエラーメッセージを取得したり、エラーの特定が容易になるという意味で使用される
    //$eに格納されている例外オブジェクトのgetMessage()メソッドを呼び出して、該当の例外のエラーメッセージを取得する。
    exit;
}
?>

後のファイルもそうだが、しつこいぐらいにコメントアウトで詳細を記録しているので、一行一行の詳しい意味合いはそちらを参考にしてもらいたい。

私はXAMPPのローカル環境を使っているが、ここは各自自身のパスワードやユーザーネームに差し替えてもらいたい。

登録ページ(register_form.php)

続いては登録画面だ。

この中のinput部分のフォームに値を入れれば、次のregister.phpというファイルにて値が渡され、登録処理が走る。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
</head>

<body>
    <div class="wrap">
        <form action="register.php" method="GET">
            <!--送信エラー時に入力値が消えないようにvalueを設定-->
            <input type="text" name="user_mail" placeholder="メールアドレス"
                value="<?php echo isset($_GET['user_mail']) ? htmlspecialchars($_GET['user_mail']) : ''; ?>"><br>
            <?php
            if (isset($_GET["error_message"])) {
                $error_message = $_GET["error_message"]; // パラメーターからエラーメッセージを取得
                if ($error_message === "メールアドレスを入力してください") {
                    echo '<div style="color: red;">メールアドレスを入力してください</div>';
                }
            }
            ?>
            <?php
                if (isset($_GET["error_message"])) {
                    $error_message = $_GET["error_message"];
                    if ($error_message === "既に使われているメールアドレスです") {
                        echo '<div style="color: red;">既に使われているメールアドレスです</div>';
                    }
                }
            ?>
            <input type="text" name="user_name" placeholder="ユーザー名"
                value="<?php echo isset($_GET['user_name']) ? htmlspecialchars($_GET['user_name']) : ''; ?>"><br>
            <?php
                if (isset($_GET["error_message"])) {
                    $error_message = $_GET["error_message"];
                    if ($error_message === "ユーザー名を入力してください") {
                        echo '<div style="color: red;">ユーザー名を入力してください</div>';
                    }
                }
            ?>
            <input type="text" name="user_pass" placeholder="パスワード"
                value="<?php echo isset($_GET['user_pass']) ? htmlspecialchars($_GET['user_pass']) : ''; ?>"><br>
            <?php
                if (isset($_GET["error_message"])) {
                    $error_message = $_GET["error_message"];
                    if ($error_message === "パスワードを入力してください") {
                        echo '<div style="color: red;">パスワードを入力してください</div>';
                    }
                }
            ?>
            <input type="submit">
        </form>
    </div>
</body>

</html>

PHPで書かれている部分は、エラーがあった際にエラー文言を出すための処理で、これも次に紹介するregister.phpから受け取る処理によって条件分岐で出し分けている。

登録システム(register.php)

いよいよ登録処理に入ってく。

ここもコメントアウトに嫌ほど解説を載せているので、意味などはそちらを参照してもらいたい。

<?php
//-----データベースへの接続-----------------------------------------------------------------------------------------------------------------------------------------------------------
include 'db.php';

//-----テーブルを作成-----------------------------------------------------------------------------------------------------------------------------------------------------------
// テーブルは作成してもこの時点では実行不可能。IF NOTを加えることでテーブルが存在していない場合の処理ができる(存在している場合はalerdy existと出る)
$create_table = <<<_TABLE_
CREATE TABLE IF NOT EXISTS users (
    user_id INT AUTO_INCREMENT PRIMARY KEY, /* ユーザーID(自動で割り振り) */
    user_name TEXT, /* ユーザー名 */
    user_pass CHAR, /* パスワード */
    user_mail TEXT, /* メールアドレス */
    user_document TEXT /* 文字保存 */
);
_TABLE_;
$result = $db->exec($create_table);//ここでテーブルが作成される

//-----フォームから受け取った情報を処理-----------------------------------------------------------------------------------------------------------------------------------------------------------
$user_name = htmlspecialchars($_GET["user_name"]);//form.phpから値を取得//エスケープ処理
$user_pass = htmlspecialchars($_GET["user_pass"]);//この時点ではパスワードはハッシュ化しない
$user_mail = htmlspecialchars($_GET["user_mail"]);
// エラー時に表示する文言を配列に格納
$error_message = array(
    "error_user_mail_same" => "既に使われているメールアドレスです",
    "error_user_mail" => "メールアドレスを入力してください",
    "error_user_name" => "ユーザー名を入力してください",
    "error_user_pass" => "パスワードを入力してください",
);

if (!empty($user_mail) && !empty($user_name) && !empty($user_pass)) {//メルアドと、名前、パスワードが空でない場合はtrue処理

    //既存ユーザー用処理。もしDBに既にあったら、insertせずにsessionIDだけ持たせる
    $check_infor = "SELECT * FROM users WHERE user_mail = :user_mail";//sql文でメルアドの入ったカラムを指定。 {$user_name}で指定すると、「SQLインジェクション」と呼ばれるセキュリティ上の脆弱性を引き起こす
    $stmt = $db->prepare($check_infor);//上記sql文の実行準備
    $stmt->bindParam(':user_mail', $user_mail);//上記のselectのsql文では、まだ特定のユーザーを指定していないので、ここで:user_mail(プレースホルダ)を$user_mail(特定のユーザー)に指定する
    $stmt->execute();//プレースホルダーにバインドされた値を含むクエリが実際にデータベースに送信。指定されたユーザー名と一致する行を検索し、結果セットを返す。
    if ($stmt->rowCount() > 0) {//rowCount() メソッドはデータベースクエリの結果セットに含まれる行数を取得し、その値が0より大きいかどうかを判定。
        //つまり特定のメルアド($user_mail)の入ったカラムを検出した$stmtの行をカウント。もし$user_mail(ユニークユーザー)の入った行が既に入っていたら
        header("location: register_form.php?error_message=" . urlencode($error_message["error_user_mail_same"]) . "&user_name=" . urlencode($user_name) . "&user_mail=" . urlencode($user_mail));
        ///エラーメッセージを選択。urlencode()はURLに含めることができない文字列や特殊文字(変数など含む)をエンコードするための関数。それをリダイレクト先に渡す。
        //$user_nameとmailもパラメーターにしているのは、送信エラーでリダイレクトされたときに入力窓の内容を表示するため(register_form.php参照)
        exit;
    } else {//もし既存ユーザーでなければ
        $user_infor = array (
            "user_name"=>$user_name,//サニタイジングした情報を連想配列+新しい変数に格納
            "user_pass"=>$user_pass = hash("sha512", $user_pass),//パスワードはハッシュ化。第一引=はハッシュアルゴリズムで第二引数は入力データ
            "user_mail"=>$user_mail,
        );
        // print_r($user_infor);//表示確認用(配列の中身を全表示)
        // echo $user_infor["user_name"];//表示確認用(配列の中身を個別に全表示)

        $query = "INSERT INTO users (user_name, user_pass, user_mail) VALUES ('{$user_infor['user_name']}', '{$user_infor['user_pass']}', '{$user_infor['user_mail']}')";
        //↑クエリ内で配列操作する場合はダブルクォートと中括弧 ({}) で囲むことで正常にsqlが実行される
        $db->exec($query);//DBに対してsql(insert)を実行

        $user_id = $db->lastInsertId();// 直前に実行されたINSERTクエリで自動生成されたユーザーIDを取得(セッションの引数に使用)
        include 'session.php';//セッション処理をまとめた関数を格納したファイル
        session_login($user_id);//ログイン時にユニークIDをセッションに保管
        ///メルアドと、名前、パスワードが入っていて、問題ない場合はルートにリダイレクト
        header("location: index.php");// ページをリロードする

        if (!empty($user_mail)) {//メールアドレスを登録しているならメール送信
            // echo "メールも成功";
            include 'mail.php';//メール送信に関する関数を格納しているmail.phpを呼びだし
            send_mail($user_name, $user_mail);//mail.php内に定義された関数に引数を加えて実行
            exit;
        }
    }

} else if (empty($user_name)) {
    header("location: register_form.php?error_message=" . urlencode($error_message["error_user_name"]) . "&user_name=" . urlencode($user_name) . "&user_mail=" . urlencode($user_mail));
    exit;
} else if (empty($user_pass)) {
    header("location: register_form.php?error_message=" . urlencode($error_message["error_user_pass"]) . "&user_name=" . urlencode($user_name) . "&user_mail=" . urlencode($user_mail));
    exit;
} else if (empty($user_mail)) {
    header("location: register_form.php?error_message=" . urlencode($error_message["error_user_mail"]) . "&user_name=" . urlencode($user_name) . "&user_mail=" . urlencode($user_mail));
    exit;
}
?>

この部分での大まかな処理の流れとしては、下記のようになっている。

  • ①先ほど作ったdb.phpをincludeし、データベースに接続
  • ②データベースにユーザーの名前やパスワードを入れるカラムを作成
  • ③フォーム画面から受け取った入力情報をサニタイジング+エラー用連想配列作成
  • ④「もしメルアド、名前、パスワードが空ではなかったら」という登録処理開始
  • ⑤「既にDBにメルアドがあれば」、エラーを出してリダイレクト
  • ⑥「DBにメルアドが登録されてい(新規)ないならば」、各情報を連想配列に入れる+配列操作でデータベースのカラムに挿入する(パスワードはhashで暗号化)
  • ⑥session.phpを呼びだし、user_idを軸にセッション処理(後に解説)
  • ⑦問題が無ければトップへリダイレクトされ、「ログイン中です」の文言表示
  • ⑧includeで呼び出したmail.phpによって登録処理が完了すると同時にメール送信処理(後に解説)
  • ⑨もしフォームが空の状態で送られてきたら、エラー文言をパラメーターに載せてフォーム画面へリダイレクト

セッション処理(session.php)

ひとつ前のregister.phpの⑥の処理にて登場したsession.phpは、ユーザーがログインしているか否かの判定として使用する。

<?php
session_start();// セッションを開始する

function session_login($user_id) {
//ログイン情報をセッションに記録
$_SESSION["login"] = array("user_id"=>$user_id);//セッションIDとしてユーザーIDを保存
}

function session_part_01($script) {//リダイレクト先のindex.phpで呼び出し
    if (isset($_SESSION['login'])) {
        if (isset($_POST['logout'])) {
            unset($_SESSION['login']);
            header("Location: $script"); // ログアウト後にページをリダイレクト(これが無いとリロードすると何も表示されなくなる)
            exit();
        }
        echo 'SessionID: ' . $_SESSION["login"]["user_id"] . '<br>'; // セッションIDとユーザーIDがリンクしているかを表示して確認
        echo '<p>ログイン中です</p>';
        echo <<<_logout_
        <form action='$script' method="POST">
            <input type="hidden" name="logout"><br>
            <input type="submit" value="ログアウトする">
        </form>
        _logout_;
        return true;//return 文は特定の条件が満たされた場合や処理を終了したい場合に使用。これがなければログアウトしたときに下記のif文も実行されてしまい、両方とも表示される
    }

    if (isset($_SESSION['logout'])) {//ログアウト状態(セッション変数にlogoutの値が入っている場合の表示)
        unset($_SESSION['login']);
        echo '<p>ログインしていません。</p>'; // ログインしていない場合の表示
        echo '<a href="form.php">ログインする</a>';
        return false;//明示的な値を指定する場合の違いが明確でない場合や、関数の戻り値を利用しない場合には、特に false や true を指定する必要ない
    }

    // ログアウト状態(セッション変数になにも入っていない場合の表示)=新規ユーザー向け
    echo '<p>ログインしていません。</p>';
    echo '<a href="register_form.php">新規登録する</a><br>';
    echo '<a href="login_form.php">ログインする</a>';
}

$script = $_SERVER["SCRIPT_NAME"]; // このPHPファイルのパス

?>

 これらをsesstion_login($user_id)という関数にしておき、register.php(もしくはlogin.php)で呼び出したとき、それらのファイルでDBから取り出したユーザーID(ユニーク)を引数として使えるようになっている。

こうすることで、$_SESSION[“login”]というスーパーグローバル変数にはユニークのユーザーIDが入ることになり、そのIDをsessionに保存する処理を行っている。

このIDがセッション情報として存在限りログイン状態となり、リダイレクト先のトップページでは「ログイン中です」と「ログアウトする」の文言が追加される。

逆にログアウトボタンを押せばフォームのhiddenで隠した「logout」という値が渡され、セッション情報がセットされなくなり、ログアウト状態となる。

新規登録時の$user_idの引数はregister.phpに定義

少しややこしいが、ユーザーはメールアドレスとユーザー名、パスワードのみ入力なのにもかかわらず、なぜユニークIDを基にセッション処理(ログイン判定)ができている。

というのも、register.phpでデータベースに接続した際にその情報も取得しているからだ。

$user_id = $db->lastInsertId();

この部分は直前にInsertクエリで実行された処理によって自動生成されたユニークのユーザーIDを取り出している。

これをsession_login($user_id);の関数として使い、セッションIDとして使用しているのだ。

メール送信処理(mail.php)

そしてregister.phpでは登録に成功したと同時にincludeでmail.phpメールを呼び出しユーザーへ飛ばしている。

<?php
function send_mail($user_name, $user_mail) {// $user_name, $user_pass, $user_mail を使用してメールの処理を行う(引数の内容はregister.phpで呼び出されたときに定義されている)
// 言語とエンコードをセット(お作法として覚えておく)
mb_language("Japanese");//日本語の文字列処理やエンコードが必要な場合には、mb_language("Japanese")を設定しておく
mb_internal_encoding("UTF-8");//文字エンコーディング
//送信者を宛名をセット
$from = "送り主のアドレス"; //送信元
$to = $user_mail; //充て先。(register.phpで定義)
//メールヘッダー作成
$encodedFrom = mb_encode_mimeheader($from);//念のため送信元をエンコードしておく(特殊な文字列が使われなければ不要)
$header = "From: $encodedFrom\n";
$header .= "Replay-to: $encodedFrom";
//件名や本文をチェック
$subject = "登録メールのテスト";
$body = "こんにちは{$user_name}さん。メールの本文(テスト)です。";//。$user_nameはregister.phpで定義
//日本語メール送信
$r = mb_send_mail($to, $subject, $body, $header);
//mb_send_mail(宛先, 件名, 本文, 追加ヘッダー); 追加ヘッダーには送信者(from)や返信先(to)が入っている
if ($r) {//もしmb_send_mail関数が実行して成功(true)したら
    echo "メール送信成功";
} else {
    echo "メール送信失敗";
}
}
?>

送り主のアドレスなどは各自で変更してもらいたいが、XAMPPを使用している場合はこの他にもsendmail.iniファイルなどを適切に設定しなければ飛ばすことができない。

下記で記録を残しているので、参考にしてもらいたい。

【PHP】簡略的なログイン機能+メール送信機能を自作してみた

ログインページ(login_form.php)

続いてはログインページだが、まずはフォーム画面からだ。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
</head>

<body>
    <div class="wrap">
        <form action="login.php" method="GET">
            <!--送信エラー時に入力値が消えないようにvalueを設定-->
            <input type="text" name="user_mail" placeholder="メールアドレス"
                value="<?php echo isset($_GET['user_mail']) ? htmlspecialchars($_GET['user_mail']) : ''; ?>"><br>
            <?php
                    if (isset($_GET["error_message"])) {
                        $error_message = $_GET["error_message"];
                        if ($error_message === "メールアドレスが違います") {
                            echo '<div style="color: red;">メールアドレスが違います</div>';
                        }
                    }
                ?>
            <?php
                    if (isset($_GET["error_message"])) {
                        $error_message = $_GET["error_message"];
                        if ($error_message === "メールアドレスを入力してください") {
                            echo '<div style="color: red;">メールアドレスを入力してください</div>';
                        }
                    }
                ?>
            <input type="text" name="user_pass" placeholder="パスワード"><br>
            <?php
                    if (isset($_GET["error_message"])) {
                        $error_message = $_GET["error_message"];
                        if ($error_message === "パスワードが違います") {
                            echo '<div style="color: red;">パスワードが違います</div>';
                        }
                    }
                ?>
            <?php
                    if (isset($_GET["error_message"])) {
                        $error_message = $_GET["error_message"];
                        if ($error_message === "パスワードを入力してください") {
                            echo '<div style="color: red;">パスワードを入力してください</div>';
                        }
                    }
                ?>
            <input type="submit">
        </form>
    </div>
</body>

</html>

新規登録フォームと似ているが、こちらはメールアドレスとパスワードのみの確認となっている。

ログイン時の処理(login.php)

ログイン時のシステムはregister.phpとかなり似ているが、要所要所で異なる部分がある。

<?php
//-----データベースへの接続-----------------------------------------------------------------------------------------------------------------------------------------------------------
include 'db.php';

//-----フォームから受け取った情報を処理-----------------------------------------------------------------------------------------------------------------------------------------------------------
$user_pass = htmlspecialchars($_GET["user_pass"]);//この時点ではパスワードはハッシュ化しない
$user_mail = htmlspecialchars($_GET["user_mail"]);
// エラー時に表示する文言を配列に格納
$error_message = array(
    "error_user_mail" => "メールアドレスが違います",
    "error_user_mail_emp" => "メールアドレスを入力してください",
    "error_user_pass" => "パスワードが違います",
    "error_user_pass_emp" => "パスワードを入力してください",
);

if (!empty($user_mail) && !empty($user_pass)) {//メルアドと、名前、パスワードが空でない場合はtrue処理

    //既存ユーザー用処理。もしDBに既にあったら、insertせずにsessionIDだけ持たせる
    $check_infor = "SELECT * FROM users WHERE user_mail = :user_mail";//sql文でメルアドの入ったカラムを指定。 {$user_name}で指定すると、「SQLインジェクション」と呼ばれるセキュリティ上の脆弱性を引き起こす
    $stmt = $db->prepare($check_infor);//上記sql文の実行準備
    $stmt->bindParam(':user_mail', $user_mail);//上記のselectのsql文では、まだ特定のユーザーを指定していないので、ここで:user_mail(プレースホルダ)を$user_mail(特定のユーザー)に指定する
    $stmt->execute();//プレースホルダーにバインドされた値を含むクエリが実際にデータベースに送信。指定されたユーザー名と一致する行を検索し、結果セットを返す。
    if ($stmt->rowCount() > 0) {//rowCount() メソッドはデータベースクエリの結果セットに含まれる行数を取得し、その値が0より大きいかどうかを判定。
        //つまり特定のメルアド($user_mail)の入ったカラムを検出した$stmtの行をカウント。もし$user_mail(ユニークユーザー)の入った行が1行あるか否かで条件分岐
        $user = $stmt->fetch(PDO::FETCH_ASSOC);//fetch() は結果セット(上記のメールに連なるカラム)から1行ずつデータを取得するために使用される。結果としてほしいカラムを抽出できる
        $hashed_password = $user['user_pass']; // データベースから取得したハッシュ化されたパスワード

        if (hash("sha512", $user_pass) === $hashed_password) {//ユーザーが入力したパスワードをハッシュ化し、DBに保存されているパスワードと一致するか確認
            // パスワードが一致する場合

            $user_id = $user['user_id'];//先ほどのdbをfetchした際に付随しているユーザーIDを配列操作で取得(セッションIDの引数として使う)
            include 'session.php';
            session_login($user_id);
            header("location: index.php");// ページをリダイレクトする
            exit;
        } else {
            //パスワードが間違っている場合
            header("location: login_form.php?error_message=" . urlencode($error_message["error_user_pass"]) . "&user_mail=" . urlencode($user_mail));
        }
    } else {//メールアドレスが間違っている場合
        header("location: login_form.php?error_message=" . urlencode($error_message["error_user_mail"]) . "&user_mail=" . urlencode($user_mail));
        exit;
    }

//どちらかが空の場合
}  else if (empty($user_pass)) {//パスワードが空の場合
    header("location: login_form.php?error_message=" . urlencode($error_message["error_user_pass_emp"]) . "&user_mail=" . urlencode($user_mail));
    exit;
} else if (empty($user_mail)) {//メールアドレスが空の場合
    header("location: login_form.php?error_message=" . urlencode($error_message["error_user_mail_emp"]) . "&user_mail=" . urlencode($user_mail));
    exit;
}


?>

register.phpに比べたら簡易的で、データベースへのINSERT処理や条件分岐が少ないため、割とすっきりしている。

ログイン時の$user_idの引数はregister.phpに定義

新規登録時と同じように、ログイン時にもデータベースからユーザーIDを取ってきて、session_login()の引数としてセッションIDとして保存する作業が必要になる。

今回ログイン時に肝になる部分は下記だ。

~~省略~~
$user = $stmt->fetch(PDO::FETCH_ASSOC);//fetch() は結果セット(上記のメールに連なるカラム)から1行ずつデータを取得するために使用される。結果としてほしいカラムを抽出できる
        $hashed_password = $user['user_pass']; // データベースから取得したハッシュ化されたパスワード
        if (hash("sha512", $user_pass) === $hashed_password) {//ユーザーが入力したパスワードをハッシュ化し、DBに保存されているパスワードと一致するか確認
            // パスワードが一致する場合
            $user_id = $user['user_id'];
            include 'session.php';
            session_login($user_id);
~~省略~~

新規登録時はlastInsertId()で最後にインサートされたIDを引数にセットしていたが、今回は$stmt->fetch~の処理により、ユーザー一人に対してDBの全てのカラムを連想配列として取得している。

これは上記のコードにも書いてある通り、ハッシュ化したパスワードの認証にも使われていたり、ユーザーIDを各引数の値としてセットする目的で使用されている。

これがsession.phpに引数としてわたり、新規登録時と同じようにセッションIDとして扱われるように処理されるのだ。

まとめ

以上がログイン機能の自作コードだったが、この後もう少し付け加えたい機能があるので、完成したらまた別の機会に公開したいと思う。

PIC UP