PHPの新規会員登録とログイン機能

学習目的

  • 会員登録 → ログイン → ログアウト → 会員専用ページ表示までの流れを作れるようになる
  • password_hash()password_verify() で安全な認証を学ぶ
  • セッションでログイン状態を管理する仕組みを理解する
  • フレームワークBootstrapの利用

フォルダとファイル名

php-db11 フォルダを作成します。ファイル構成は以下になります。

php-db11/
├── common.php        # DB接続と共通関数
├── register.php      # 会員登録フォーム
├── register_done.php # 登録完了ページ
├── login.php         # ログインフォーム
├── auth.php          # ログイン認証(DBから照合)
├── member.php        # ログイン後の会員専用ページ
├── logout.php        # ログアウト処理
└── members.sql       # 会員テーブル作成用SQL

フローチャート

register.php(会員登録フォーム:ユーザー名・パスワード入力)
 ↓
register_done.php(データベースに保存)
 ↓
login.php(ログインフォーム)
 ↓
auth.php(ログイン認証:データベースと照合)
 →  成功 → member.php(会員専用ページ)
 →  失敗 → login.php に戻る
 ↓
logout.php(セッション情報の破棄)
 → login.php に戻る

完成イメージ

データベース:会員テーブル作成

まずはmenbersテーブルを用意します。

CREATE TABLE members (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • パスワードは必ず password_hash() で暗号化して保存。
  • username を UNIQUE にして、同じユーザー名は登録できないように。

1.common.php(共通処理)

php-db09 とほぼ同じですが、固定の define を削除し、DB接続を共通化にしておきます。

<?php
session_start();

// DB接続
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$user = 'root';
$password = '';

try {
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo "DB接続エラー:" . $e->getMessage();
    exit;
}

// エスケープ関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

2.register.php(会員登録フォーム)+bootstrap

新規会員登録のフォーム ユーザー名とパスワードを入力

<?php require 'common.php'; ?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>新規会員登録</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <main class="container mt-5" style="max-width: 500px;">
        <div class="card shadow-sm">
            <div class="card-body">
                <h1 class="h4 mb-4 text-center">新規会員登録</h1>

                <form action="register_done.php" method="post">
                    <div class="mb-3">
                        <label for="username" class="form-label">ユーザー名</label>
                        <input type="text" name="username" id="username" class="form-control" required>
                    </div>

                    <div class="mb-3">
                        <label for="password" class="form-label">パスワード</label>
                        <input type="password" name="password" id="password" class="form-control" required>
                    </div>

                    <button type="submit" class="btn btn-primary w-100">登録</button>
                </form>

                <p class="mt-3 text-center">
                    <a href="login.php">ログインはこちら</a>
                </p>
            </div>
        </div>
    </main>
</body>

</html>

3.register_done.php(登録処理)

<?php
require 'common.php';

$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');

// バリデーション
if ($username === '' || $password === '') {
    exit('ユーザー名とパスワードは必須です。');
}

// パスワードをハッシュ化
$hashed = password_hash($password, PASSWORD_DEFAULT);

// 登録
try {
    $stmt = $pdo->prepare("INSERT INTO members (username, password) VALUES (?, ?)");
    $stmt->execute([$username, $hashed]);
    echo "登録完了!<a href='login.php'>ログインする</a>";
} catch (PDOException $e) {
    if ($e->getCode() === '23000') { // UNIQUE制約エラー
        echo "このユーザー名は既に使われています。<a href='register.php'>戻る</a>";
    } else {
        echo "エラー:" . $e->getMessage();
    }
}

$stmtという名前の由来

$stmt という変数名は 「ステートメント (statement)」 の略。PDO で SQL を実行する手順は、

  1. SQL文を準備する(prepare)
  2. 実行する(execute)
  3. 結果を取り出す(fetch)

という流れです。ここで prepare() が返すのは PDOStatementオブジェクト と呼ばれるもので、
このオブジェクトを通じて execute()fetch() を呼び出します。

stmt は statement(ステートメント)の略。PDOStatementオブジェクトが入る変数です。

そのため変数名は statement の略で $stmt と書かれることが多いです。

password_hash() とは?

password_hash() は、パスワードを安全に保存するためのPHP関数です。

入力されたパスワードを「ハッシュ化」という方法で変換し、元の文字列が分からない形にしてからデータベースに保存します。

  • 保存するのは元のパスワードではなく、暗号化された文字列
  • PASSWORD_DEFAULTで、60文字のハッシュ文字
  • 毎回違う値になるが、password_verify() で一致判定ができる
  • セキュリティ上、必ず平文ではなくハッシュ化して保存する

60文字だからといって データベーステーブル作成時にVARCHAR(60) にすると、将来 PASSWORD_DEFAULT のアルゴリズムが変わったときに収まらない可能性があります。公式ドキュメントでも「最低255文字のカラム長を推奨」とあります

getCode() メソッド

if ($e->getCode() === '23000') { // UNIQUE制約エラー
    echo "このユーザー名は既に使われています。<a href='register.php'>戻る</a>";
}
  • $ecatch (PDOException $e) で受け取った例外オブジェクトです。
    getCode() メソッドを使うと、データベースから返されたエラーコードを取得できます。(DBのTable作成時にユニーク(一意)設定している)
  • 23000SQLSTATE エラーコード で、主に「UNIQUE制約違反」や「外部キー制約違反」が発生したときに返されます。この場合は「同じユーザー名がすでに登録されている」状態を表しています。
  • 条件が一致した場合、ユーザーに「このユーザー名は既に使われています」と表示し、
    <a href='register.php'>戻る</a> で登録フォームに戻るリンクを出しています。

主なSQLSTATEコード例

コード意味
23000整合性制約違反(UNIQUE制約、外部キー制約)
42S02テーブルが存在しない
42000SQL構文エラー
HY000一般的なエラー(キャッチオール)
28000認証エラー(ログイン失敗)

課題:バリデーションを追加してみよう

今回はユーザー名とパスワードが空かどうかだけをチェックしましたが、下記のようにもう少し厳密なバリデーションを追加することもできます。パスワードを設定した場合は、エラー表示の設定も必要です。

// ユーザー名が50文字以内かチェック
if (mb_strlen($username) > 50) {
    exit('ユーザー名は50文字以内で入力してください。');
}

// パスワードが8文字以上、英字と数字を含むかチェック
if (!preg_match('/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/', $password)) {
    exit('パスワードは8文字以上で英字と数字を含めてください。');
}

4.login.php (ログイン)

<?php require 'common.php'; ?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <main class="container mt-5" style="max-width: 500px;">
        <div class="card shadow-sm">
            <div class="card-body">
                <h1 class="h4 mb-4 text-center">ログイン</h1>
                <form action="auth.php" method="post">
                    <div class="mb-3">
                        <label for="username" class="form-label">ユーザー名</label>
                        <input type="text" name="username" id="username" class="form-control" required autofocus>
                    </div>

                    <div class="mb-3">
                        <label for="password" class="form-label">パスワード</label>
                        <input type="password" name="password" id="password" class="form-control" required>
                    </div>
                    <button type="submit" class="btn btn-primary w-100">ログイン</button>
                </form>

                <p class="mt-3 text-center">
                    <a href="register.php">新規会員登録はこちら</a>
                </p>
            </div>
        </div>
    </main>
</body>

</html>

5.auth.php(認証処理)

<?php
require 'common.php';

$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');

// ユーザー検索
$stmt = $pdo->prepare("SELECT * FROM members WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// 認証判定
if ($user && password_verify($password, $user['password'])) {
    // ログイン成功 → セッションに保存
    $_SESSION['member'] = $user['username'];
    header('Location: member.php');
    exit;
} else {
    // ログイン失敗 → シンプルなメッセージ
    echo "ユーザー名またはパスワードが違います。<a href='login.php'>戻る</a>";
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン失敗</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <div class="container mt-5">
          <div class="col-md-6 mx-auto">
                <div class="alert alert-danger text-center" role="alert">
<!-- role="alert" はスクリーンリーダーに「重要な警告」として読ませるための属性 -->
                    ユーザー名またはパスワードが違います。
                </div>
                <div class="text-center">
                    <a href="login.php" class="btn btn-outline-primary">ログイン画面に戻る</a>
                </div>
            </div>
    </div>
</body>

</html>

password_verify()

password_verify() は、ログイン時に入力されたパスワードと
データベースに保存されているハッシュ化済みのパスワードを照合するための関数です。

  • 第1引数:ユーザーが入力したパスワード(平文)
  • 第2引数:データベースから取得したハッシュ化パスワード
  • 戻り値は true(一致)または false(不一致)

password_hash()password_verify() は ペアで使う関数です。

  • 登録時 → password_hash() でハッシュ化して保存
  • ログイン時 → password_verify() で比較

ハッシュ化は一方向なので、元のパスワードに復号はできません。
代わりに password_verify() が同じパスワードかどうかを判定してくれます。

ハッシュ化(hashing)とは?

  • パスワードなどのデータを「元に戻せない形」に変換する処理です。
  • 登録時はハッシュ化した値をデータベースに保存
  • ログイン時は入力パスワードを再びハッシュ化して照合
  • こうすることで、データベースが盗まれても元のパスワードが分からないようにできます。

ログイン情報の保存(セッション変数)

$_SESSION['member'] = $user['username']; は、ログイン成功時にユーザー名をセッションに保存する処理です。

  • セッション変数は、ページを移動してもデータを保持できる特別な変数です。
  • ここでは、データベースから取得したユーザー名を $_SESSION['member'] に代入
  • 他のページでも $_SESSION['member'] を使えば、ログイン中のユーザー名がわかる
  • ログアウトするときにセッションを破棄すると、この情報も消える

6.member.php(会員専用ページ)

<?php
require 'common.php';
if (empty($_SESSION['member'])) {
    header('Location: login.php');
    exit;
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>会員専用ページ</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <main class="container mt-5">
     <div class="col-md-8 mx-auto">
        <div class="card shadow-sm">
            <div class="card-body text-center">
                <h1 class="h4 mb-4">
                    ようこそ <span class="text-primary"><?= h($_SESSION['member']) ?></span> さん
                </h1>
                <p class="mb-4">このページは会員専用コンテンツです。</p>
                <a href="logout.php" class="btn btn-outline-danger w-100">ログアウト</a>
            </div>
        </div>
    </div>
    </main>
</body>

</html>

7.logout.php(ログアウト処理)

<?php
require 'common.php';

// セッションの中身を空にする
$_SESSION = [];

// セッションを完全に破棄
if (session_id() !== '' || isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 42000, '/');
}
session_destroy();

// ログインページへリダイレクト
header('Location: login.php');
exit;

セッションIDのクッキーも削除する理由

ログアウト時に session_destroy() を実行すると、サーバー側のセッションデータは破棄されますが、
ブラウザに残っている「セッションIDのクッキー」は削除されません。

そのため、より安全にするために、以下のコードでセッションIDのクッキーも無効化しています。

// セッションを完全に破棄(クッキーも削除)
if (session_id() !== '' || isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 42000, '/');
}
session_destroy();
  • これにより、ブラウザ側のセッションIDも消える
  • 他のページに移動しても、ログイン情報が残らないセキュリティを高めるために推奨される方法
  • time() – 42000 は「過去の時刻」を指定してクッキーを期限切れにするための仕組み(42000(11時間前は、慣例的な数値) という数値自体に特別な意味はなく、たとえば time() - 3600(1時間前)でも問題ありません