驗證碼是防止機器人自動提交的第一道防線。本文提供生產級驗證碼實作:真彩色畫布、干擾線/點雜訊、字元扭曲、TTF 中文支援、Session 安全儲存與 AJAX 驗證流程。
<?php
declare(strict_types=1);
session_start();
// 設定參數
$width = 130;
$height = 45;
$codeLength = 5;
$fontSize = 22;
$fontFile = __DIR__ . '/fonts/NotoSansSC-Regular.otf'; // 需下載中文字體
$charset = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz'; // 排除易混淆
// 建立真彩色畫布
$im = imagecreatetruecolor($width, $height);
// 背景色 (淺色系)
$bgColor = imagecolorallocate($im, 245, 245, 250);
imagefill($im, 0, 0, $bgColor);
// 干擾線 (3-5 條)
for ($i = 0, $count = random_int(3, 5); $i < $count; $i++) {
$color = imagecolorallocate($im, random_int(180, 255), random_int(180, 255), random_int(180, 255));
imageline($im,
random_int(0, $width), random_int(0, $height),
random_int(0, $width), random_int(0, $height),
$color
);
}
// 干擾點 (50-100 個)
for ($i = 0, $count = random_int(50, 100); $i < $count; $i++) {
$color = imagecolorallocate($im, random_int(0, 255), random_int(0, 255), random_int(0, 255));
imagesetpixel($im, random_int(0, $width), random_int(0, $height), $color);
}
// 干擾弧線 (增加識別難度)
for ($i = 0; $i < 2; $i++) {
$color = imagecolorallocate($im, random_int(150, 220), random_int(150, 220), random_int(150, 220));
imagearc($im,
random_int(-10, $width), random_int(-10, $height),
random_int(50, $width * 2), random_int(50, $height * 2),
random_int(0, 360), random_int(0, 360),
$color
);
}
// 生成隨機碼
$code = '';
for ($i = 0; $i < $codeLength; $i++) {
$code .= $charset[random_int(0, strlen($charset) - 1)];
}
// 安全儲存至 Session (雜湊存儲,防洩露)
$_SESSION['captcha_hash'] = password_hash(strtoupper($code), PASSWORD_DEFAULT);
$_SESSION['captcha_time'] = time();
// 繪製文字
$textColor = imagecolorallocate($im, random_int(20, 80), random_int(20, 80), random_int(20, 80));
$x = 12;
$charWidth = ($width - 24) / $codeLength;
for ($i = 0; $i < $codeLength; $i++) {
$char = $code[$i];
$y = random_int(28, 36);
$angle = random_int(-18, 18);
if (file_exists($fontFile) && function_exists('imagettftext')) {
// TTF 字體 (支援中文、反鋸齒)
imagettftext($im, $fontSize, $angle, $x, $y, $textColor, $fontFile, $char);
$x += $charWidth + random_int(-2, 2);
} else {
// 內建字體 (備援)
imagestring($im, 5, $x, random_int(10, 14), $char, $textColor);
$x += 22;
}
}
// 輸出
header('Content-Type: image/png');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
imagepng($im);
imagedestroy($im);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>驗證碼測試</title>
<style>
.captcha-img { cursor: pointer; vertical-align: middle; border: 1px solid #ddd; }
.input-group { display: flex; align-items: center; gap: 10px; margin: 10px 0; }
</style>
</head>
<body>
<form id="test-form">
<div class="input-group">
<input type="text" name="captcha" id="captcha-input" placeholder="輸入驗證碼" required>
<img id="captcha-img" class="captcha-img" src="captcha.php" alt="驗證碼">
<button type="button" id="refresh-captcha">換一張</button>
</div>
<button type="submit">提交驗證</button>
</form>
<script>
document.getElementById('refresh-captcha').addEventListener('click', function () {
// 加時間戳防快取
document.getElementById('captcha-img').src = 'captcha.php?t=' + Date.now();
});
document.getElementById('test-form').addEventListener('submit', async function (e) {
e.preventDefault();
const code = document.getElementById('captcha-input').value.trim();
if (!code) return alert('請輸入驗證碼');
const formData = new FormData();
formData.append('captcha', code);
try {
const res = await fetch('verify.php', { method: 'POST', body: formData });
const result = await res.json();
alert(result.message);
if (result.success) {
// 成功後重新整理驗證碼
document.getElementById('captcha-img').src = 'captcha.php?t=' + Date.now();
document.getElementById('captcha-input').value = '';
}
} catch (err) {
console.error(err);
alert('請求失敗');
}
});
</script>
</body>
</html>
<?php
declare(strict_types=1);
session_start();
header('Content-Type: application/json; charset=utf-8');
$input = trim($_POST['captcha'] ?? '');
if ($input === '') {
echo json_encode(['success' => false, 'message' => '請輸入驗證碼']);
exit;
}
// 取得 Session 中的雜湊
$hash = $_SESSION['captcha_hash'] ?? '';
$time = $_SESSION['captcha_time'] ?? 0;
// 時效檢查 (5 分鐘)
if (time() - $time > 300) {
echo json_encode(['success' => false, 'message' => '驗證碼已過期,請重新獲取']);
unset($_SESSION['captcha_hash'], $_SESSION['captcha_time']);
exit;
}
// 驗證 (不分大小寫)
if ($hash && password_verify(strtoupper($input), $hash)) {
// 驗證成功:銷毀一次性驗證碼
unset($_SESSION['captcha_hash'], $_SESSION['captcha_time']);
echo json_encode(['success' => true, 'message' => '驗證通過!']);
} else {
echo json_encode(['success' => false, 'message' => '驗證碼錯誤']);
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 圖片顯示破圖 | 輸出前有空白/BOM/echo | 檢查檔案編碼 (UTF-8 無 BOM)、啟用 output_buffering |
| imagettftext 無效 | FreeType 未編譯或字體路徑錯誤 | phpinfo() 確認 FreeType Support、使用絕對路徑 |
| 中文顯示方塊 | 字體不支援中文 | 下載 Noto Sans SC、Source Han Sans 等支援 CJK 的字體 |
| Session 驗證失效 | Session 未啟動/路徑/域名不匹配 | 確認 session_start()、cookie 設定、跨子域共享 |