iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Claude AI

從零打造圖書管理系統:WSL2 × MySQL × Claude Code 的整合實作系列 第 16 篇

Day 16 會員登入實作、password_verify 與 Session 狀態管理

  • 分享至 

  • xImage
  •  

完成會員註冊後,今天要實作「會員登入」功能。

當使用者輸入帳號與密碼時,後端需要做兩件事:

  1. 驗證身分:比對使用者輸入的密碼,跟資料庫裡儲存的 password_hash() 亂碼是否吻合。
  2. 記住狀態:HTTP 協定本身是「無狀態」的,每次發送請求都是獨立事件。必須透過 Session 機制,讓伺服器記住「這個使用者已經登入成功了」。

驗證密碼的核心:password_verify()

在上一篇文章有提到,密碼經過雜湊後是不可逆的,既然無法解密,該怎麼驗證使用者輸入的密碼是否正確?

PHP 提供了一個非常方便且安全的函式:password_verify()。

// 語法:password_verify(使用者輸入的明文密碼, 資料庫裡的雜湊碼)
\(isPasswordCorrect = password_verify(\)inputPassword, $dbPasswordHash);

password_verify() 會自動分析資料庫那串雜湊碼裡的「鹽值」與演算法參數,拿使用者這次輸入的密碼做相同的數學計算。如果算出來結果一致,就會回傳 true,否則回傳 false。

讓 Claude Code 幫忙產生登入功能

在專案目錄下啟動 Claude Code 下達需求:

請幫我寫一個 login.php。前端要包含帳號(Email)與密碼表單;後端接收 POST 請求後,查詢 members 資料表是否存在該帳號。如果有找到,使用 password_verify() 驗證密碼。密碼正確時,啟動 Session 並將會員 ID 與姓名存入 $_SESSION,最後導向首頁 index.php;若驗證失敗,顯示適當的錯誤訊息。

生成的 login.php:

<?php

// ============================================================
// 區塊:啟動 Session
// ============================================================
session_start(); // 必須在任何 HTML 輸出之前呼叫,之後才能讀寫 $_SESSION

// ============================================================
// 區塊:載入資料庫連線
// ============================================================
require __DIR__ . '/config/database.php'; // 載入 Database 類別定義

$pdo = Database::getConnection(); // 取得 PDO 連線實例

// ============================================================
// 區塊:初始化表單狀態
// ============================================================
$errors = []; // 存放錯誤訊息的陣列
$email = ''; // 帳號(email)欄位,登入失敗重新顯示表單時用來保留使用者輸入

// ============================================================
// 區塊:處理表單送出(POST)
// ============================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 只有 POST 才進行登入驗證,GET 只顯示空白表單
    $email = trim($_POST['email'] ?? ''); // 取得帳號(email)並去除頭尾空白
    $password = (string) ($_POST['password'] ?? ''); // 取得密碼,密碼不 trim 以免誤刪使用者刻意輸入的空白
    // ------------------------------------------------------------
    // 子區塊:檢查必填欄位是否為空
    // ------------------------------------------------------------
    if ($email === '') {
        $errors[] = '請輸入帳號 (email)'; // 帳號為空
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = '帳號格式不正確,請輸入有效的 email'; // 帳號格式錯誤
    }
    if ($password === '') {
        $errors[] = '請輸入密碼'; // 密碼為空
    }
    // ------------------------------------------------------------
    // 子區塊:查詢 members 資料表並驗證密碼
    // ------------------------------------------------------------
    if (empty($errors)) { // 前面欄位驗證都通過才查資料庫,避免無意義的查詢
        $sql = 'SELECT id, name, password FROM members WHERE email = :email'; // 依 email 查出會員 ID、姓名與密碼雜湊
        $stmt = $pdo->prepare($sql); // 準備查詢語句,使用 prepared statement 防止 SQL injection
        $stmt->execute([':email' => $email]); // 綁定帳號並執行查詢
        $member = $stmt->fetch(); // 查無資料時回傳 false

        // 找到帳號且 password_verify() 比對雜湊成功,才算登入成功
        if ($member !== false && password_verify($password, $member['password'])) {
            session_regenerate_id(true); // 登入成功後更換 Session ID,防止 session fixation 攻擊

            $_SESSION['member_id'] = $member['id']; // 存入會員 ID,供後續頁面判斷登入者
            $_SESSION['member_name'] = $member['name']; // 存入會員姓名,供頁面顯示

            header('Location: index.php'); // 登入成功導向首頁
            exit; // 導向後立刻結束,避免下方 HTML 繼續輸出
        }

        // 帳號不存在與密碼錯誤顯示同一則訊息,避免讓人藉此猜出哪些 email 已註冊
        $errors[] = '帳號或密碼錯誤,請重新輸入';
    }
}

?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>會員登入 - 圖書管理系統</title>
<style>
    body {
        font-family: "Microsoft JhengHei", Arial, sans-serif;
        background-color: #f5f5f5;
        color: #333;
        margin: 0;
        padding: 2rem;
    }
    h1 {
        text-align: center;
        margin-bottom: 1.5rem;
    }
    form {
        width: 100%;
        max-width: 480px;
        margin: 0 auto;
        background-color: #fff;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        padding: 1.5rem 2rem;
        box-sizing: border-box;
    }
    .field {
        margin-bottom: 1rem;
    }
    label {
        display: block;
        margin-bottom: 0.35rem;
        font-weight: bold;
    }
    input[type="email"],
    input[type="password"] {
        width: 100%;
        padding: 0.5rem;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        font-size: 1rem;
    }
    button {
        width: 100%;
        padding: 0.75rem;
        background-color: #2c3e50;
        color: #fff;
        border: none;
        border-radius: 4px;
        font-size: 1rem;
        cursor: pointer;
    }
    button:hover {
        background-color: #1a252f;
    }
    .errors {
        max-width: 480px;
        margin: 0 auto 1rem;
        background-color: #fdecea;
        color: #b71c1c;
        border: 1px solid #f5c6cb;
        border-radius: 4px;
        padding: 0.75rem 1rem;
    }
    .errors ul {
        margin: 0;
        padding-left: 1.25rem;
    }
    .back-link {
        display: block;
        max-width: 480px;
        margin: 1rem auto 0;
        text-align: center;
    }
</style>
</head>
<body>
     <h1>會員登入</h1>

<?php // 區塊:顯示欄位驗證或登入失敗的錯誤訊息 ?>
<?php if (!empty($errors)): ?>
    <div class="errors">
        <ul>
<?php foreach ($errors as $error): ?>
            <li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
        </ul>
    </div>
<?php endif; ?>

    <!-- 區塊:登入表單,欄位包含帳號(email)與密碼 -->
    <form method="POST" action="login.php">
        <div class="field">
            <label for="email">帳號 (email)</label>
            <input type="email" id="email" name="email" value="<?= htmlspecialchars($email) ?>" required>
        </div>
        <div class="field">
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">登入</button>
    </form>
     <a class="back-link" href="register.php">還沒有帳號?前往註冊</a>
</body>
</html>

會員登入

開發實務與資安細節解析

  1. session_start() 的位置:
    在任何需要讀取或寫入 $_SESSION 的 PHP 檔案最上方,都必須呼叫 session_start()。而且它前面不能有任何 HTML 輸出或空白行,否則會觸發 Headers already sent 警告。
  2. Prepared Statements 防禦:
    查詢帳號時同樣使用 PDO 綁定參數(:email),防止駭客在帳號輸入框嘗試使用 ' OR '1'='1 進行 SQL Injection 繞過驗證。

瀏覽器驗證與測試流程

  1. 正常登入測試:進入 login.php,輸入註冊好的帳號(user1@example.com)與密碼。按下登入後,成功跳轉至 index.php。
  2. 錯誤密碼測試:故意輸入錯誤的密碼,頁面成功攔截並在上方紅框顯示「帳號或密碼錯誤」。
    錯誤密碼
  3. 檢查 Cookie 與 Session:打開瀏覽器開發者工具(F12)-> Application -> Cookies,可以看到瀏覽器存了一個名為 PHPSESSID 的 Cookie。這串 Session ID 就是伺服器用來辨識目前使用者身分的通行證。

今天完成了會員登入功能,並利用 password_verify() 與 Session 建立起使用者的登入狀態。

明天將把 Session 應用在整個系統中,實作「登出功能」與「未登入使用者的權限控管」,確保只有登入的會員才能看到或操作特定功能。


上一篇
Day 15 會員註冊與 password_hash()
下一篇
Day 17 借書功能 — 檢查庫存、紀錄借閱與書籍數量的邏輯
系列文
從零打造圖書管理系統:WSL2 × MySQL × Claude Code 的整合實作 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言