架構概覽
- 前端:HTML5 表單 + jQuery 3.x 驗證與 AJAX 提交
- 後端:PHP 7+ + MySQLi (Prepared Statement 防注入)
- 資料交換:JSON 格式
- 安全:輸入驗證、參數化查詢、XSS 過濾、CSRF Token (建議補充)
前端頁面 (index.html)
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>會員註冊 - AJAX 示範</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {font-family: "Microsoft JhengHei", sans-serif; text-align:center; padding-top:50px;}
.form-box {display:inline-block; text-align:left; border:1px solid #ddd; padding:20px; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,.1);}
.form-row {margin-bottom:15px;}
label {display:inline-block; width:100px; text-align:right; margin-right:10px; font-weight:bold;}
input[type=text], input[type=password], input[type=email] {padding:8px; border:1px solid #ccc; border-radius:4px; width:250px;}
.btn {padding:10px 24px; background:#007bff; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:16px;}
.btn:hover {background:#0056b3;}
.msg {margin-top:15px; font-weight:bold;}
.error {color:#dc3545;} .success {color:#28a745;}
.loading {color:#6c757d;}
</style>
</head>
<body>
<div class="form-box">
<h2>會員註冊</h2>
<form id="regForm">
<div class="form-row">
<label>帳號:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-row">
<label>年齡:</label>
<input type="number" id="age" name="age" min="1" max="120" required>
</div>
<div class="form-row">
<label>密碼:</label>
<input type="password" id="password" name="password" minlength="6" maxlength="20" required>
</div>
<div class="form-row">
<label>地址:</label>
<input type="text" id="address" name="address" required>
</div>
<div class="form-row">
<label>Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-row">
<button type="submit" class="btn">註冊</button>
<button type="reset" class="btn" style="background:#6c757d;">重置</button>
</div>
</form>
<div id="result" class="msg"></div>
<div id="loading" class="msg loading" style="display:none;">處理中...</div>
</div>
<script>
$(function(){
$('#regForm').on('submit', function(e){
e.preventDefault();
var $form = $(this), $btn = $form.find('button[type=submit]'), $msg = $('#result'), $loading = $('#loading');
// 前端驗證
var username = $.trim($('#username').val());
var age = $.trim($('#age').val());
var password = $.trim($('#password').val());
var address = $.trim($('#address').val());
var email = $.trim($('#email').val());
if(!username){ alert('請輸入帳號'); return; }
if(!age || !/^\d+$/.test(age)){ alert('請輸入有效年齡'); return; }
if(password.length < 6 || password.length > 20){ alert('密碼長度 6-20 字元'); return; }
if(!address){ alert('請輸入地址'); return; }
if(!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){ alert('Email 格式錯誤'); return; }
$btn.prop('disabled', true);
$loading.show();
$msg.removeClass('error success').text('');
$.ajax({
url: '_process.php',
type: 'POST',
dataType: 'json',
data: {
username: username,
age: age,
password: password,
address: address,
email: email
},
success: function(res){
$loading.hide();
$btn.prop('disabled', false);
if(res.success){
$msg.addClass('success').text(res.message);
$form[0].reset();
}else{
$msg.addClass('error').text(res.message);
}
},
error: function(xhr){
$loading.hide();
$btn.prop('disabled', false);
$msg.addClass('error').text('伺服器錯誤:' + xhr.status);
}
});
});
});
</script>
</body>
</html>
後端處理 (_process.php)
<?php
header('Content-Type: application/json; charset=utf-8');
// 資料庫連線 (建議改用環境變數/設定檔管理憑證)
$mysqli = new mysqli('localhost', 'root', 'root', 'register');
if ($mysqli->connect_errno) {
echo json_encode(['success'=>false, 'message'=>'資料庫連線失敗: '.$mysqli->connect_error]);
exit;
}
$mysqli->set_charset('utf8mb4');
// 接收並清理輸入
$username = trim($_POST['username'] ?? '');
$age = isset($_POST['age']) ? (int)$_POST['age'] : 0;
$password = trim($_POST['password'] ?? '');
$address = trim($_POST['address'] ?? '');
$email = trim($_POST['email'] ?? '');
// 伺服端二次驗證
$errors = [];
if ($username === '') $errors[] = '帳號不可為空';
if ($age <= 0) $errors[] = '年齡需為正整數';
if ($password === '') $errors[] = '密碼不可為空';
if ($address === '') $errors[] = '地址不可為空';
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email 格式錯誤';
if ($errors) {
echo json_encode(['success'=>false, 'message'=>implode(';', $errors)]);
exit;
}
// 檢查帳號唯一性
$stmt = $mysqli->prepare("SELECT 1 FROM tb_register WHERE username = ? LIMIT 1");
$stmt->bind_param('s', $username);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
echo json_encode(['success'=>false, 'message'=>'該帳號已被註冊']);
$stmt->close();
$mysqli->close();
exit;
}
$stmt->close();
// 密碼雜湊 (生產環境必須)
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// 寫入資料
$addTime = date('Y-m-d H:i:s');
$stmt = $mysqli->prepare("INSERT INTO tb_register (username, age, password, address, email, add_time) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->bind_param('sissss', $username, $age, $passwordHash, $address, $email, $addTime);
$success = $stmt->execute();
$stmt->close();
$mysqli->close();
echo json_encode([
'success' => $success,
'message' => $success ? '註冊成功!' : '註冊失敗,請稍後再試'
]);
?>
資料表結構 (MySQL)
CREATE TABLE `tb_register` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`age` TINYINT UNSIGNED NOT NULL,
`password` VARCHAR(255) NOT NULL, -- 存雜湊
`address` VARCHAR(255) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`add_time` DATETIME NOT NULL,
INDEX `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
安全性強化清單
| 防護層面 |
措施 |
本範例狀態 |
| SQL 注入 |
預處理語句 |
✅ 已實作 |
| XSS |
輸出時 `htmlspecialchars` / JSON 編碼 |
✅ JSON 自動編碼 |
| CSRF |
同源驗證 / CSRF Token |
⚠️ 範例未含,生產必加 |
| 密碼儲存 |
bcrypt/argon2 雜湊 |
✅ `password_hash` |
| 傳輸加密 |
HTTPS |
⚠️ 部署層面配置 |
| 頻率限制 |
CAPTCHA / IP 限流 |
⚠️ 建議加入 |
除錯技巧
- 瀏覽器 DevTools → Network → 查看 `_process.php` Request/Response Payload。
- PHP 錯誤日誌:`tail -f /var/log/php-fpm/error.log`。
- MySQL 慢查詢日誌分析長耗時 SQL。