驗證碼是表單防刷基礎設施。本文實現功能完整的驗證碼類,支援多字符集、TTF 字體、幹擾線/點/曲線、Session 存儲、AJAX 刷新、無障礙語音驗證碼接口,安全易用。
config = array_merge([
'width' => 130,
'height' => 50,
'length' => 4,
'charset' => '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // 排除易混淆字符
'fontSize' => 24,
'fontFile' => __DIR__ . '/fonts/DejaVuSans-Bold.ttf', // 需提供 TTF
'bgColor' => [240, 240, 240],
'useCurve' => true, // 貝塞爾曲線干擾
'useNoise' => true, // 噪點
'useLines' => true, // 干擾線
'sessionKey' => 'captcha_code',
'expire' => 600, // 10分鐘過期
], $config);
}
public function generate() {
session_start();
// 1. 創建畫布
$this->image = imagecreatetruecolor($this->config['width'], $this->config['height']);
// 2. 背景填充
$bg = imagecolorallocate($this->image, ...$this->config['bgColor']);
imagefill($this->image, 0, 0, $bg);
// 3. 幹擾元素 (先繪製,字符在上層)
if ($this->config['useNoise']) $this->drawNoise(80);
if ($this->config['useLines']) $this->drawLines(4);
if ($this->config['useCurve']) $this->drawBezierCurve(2);
// 4. 生成字符並繪製
$this->code = $this->generateCode();
$this->drawChars();
// 5. 存入 Session (小寫統一驗證)
$_SESSION[$this->config['sessionKey']] = strtolower($this->code);
$_SESSION[$this->config['sessionKey'] . '_time'] = time();
// 6. 輸出圖片
header('Content-Type: image/png');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
imagepng($this->image);
imagedestroy($this->image);
return $this->code;
}
private function generateCode(): string {
$len = strlen($this->config['charset']);
$code = '';
for ($i = 0; $i < $this->config['length']; $i++) {
$code .= $this->config['charset'][random_int(0, $len - 1)];
}
return $code;
}
private function drawChars() {
$width = $this->config['width'];
$height = $this->config['height'];
$length = $this->config['length'];
$fontSize = $this->config['fontSize'];
$fontFile = $this->config['fontFile'];
$charWidth = ($width - 20) / $length;
$startX = 10;
for ($i = 0; $i < $length; $i++) {
$char = $this->code[$i];
$x = $startX + $i * $charWidth + random_int(-2, 2);
$y = random_int($height * 0.4, $height * 0.7);
$angle = random_int(-15, 15);
$color = imagecolorallocate($this->image,
random_int(30, 120), random_int(30, 120), random_int(30, 120));
// TTF 字體繪製 (需 GD 支援 FreeType)
if (file_exists($fontFile) && function_exists('imagettftext')) {
imagettftext($this->image, $fontSize, $angle, $x, $y, $color, $fontFile, $char);
} else {
// 降級內置字體
$color = imagecolorallocate($this->image, 0, 0, 0);
imagestring($this->image, 5, $x, $y - 10, $char, $color);
}
}
}
private function drawNoise(int $count) {
for ($i = 0; $i < $count; $i++) {
$color = imagecolorallocate($this->image,
random_int(150, 255), random_int(150, 255), random_int(150, 255));
imagesetpixel($this->image, random_int(0, $this->config['width']), random_int(0, $this->config['height']), $color);
}
}
private function drawLines(int $count) {
for ($i = 0; $i < $count; $i++) {
$color = imagecolorallocate($this->image,
random_int(100, 200), random_int(100, 200), random_int(100, 200));
imageline($this->image,
random_int(0, $this->config['width']), random_int(0, $this->config['height']),
random_int(0, $this->config['width']), random_int(0, $this->config['height']),
$color);
}
}
private function drawBezierCurve(int $count) {
for ($i = 0; $i < $count; $i++) {
$color = imagecolorallocate($this->image,
random_int(80, 180), random_int(80, 180), random_int(80, 180));
// 三次貝塞爾曲線: 起點、控制點1、控制點2、終點
$points = [
random_int(0, $this->config['width'] * 0.2),
random_int(0, $this->config['height']),
random_int($this->config['width'] * 0.3, $this->config['width'] * 0.7),
random_int(0, $this->config['height']),
random_int($this->config['width'] * 0.3, $this->config['width'] * 0.7),
random_int(0, $this->config['height']),
random_int($this->config['width'] * 0.8, $this->config['width']),
random_int(0, $this->config['height']),
];
// 近似繪製: 分段連線
$segments = 20;
for ($t = 0; $t <= $segments; $t++) {
$t /= $segments;
$x = pow(1-$t,3)*$points[0] + 3*pow(1-$t,2)*$t*$points[2] + 3*(1-$t)*pow($t,2)*$points[4] + pow($t,3)*$points[6];
$y = pow(1-$t,3)*$points[1] + 3*pow(1-$t,2)*$t*$points[3] + 3*(1-$t)*pow($t,2)*$points[5] + pow($t,3)*$points[7];
if ($t > 0) imageline($this->image, $lastX, $lastY, $x, $y, $color);
$lastX = $x; $lastY = $y;
}
}
}
public static function verify(string $input, string $sessionKey = 'captcha_code'): bool {
session_start();
$stored = strtolower($_SESSION[$sessionKey] ?? '');
$time = $_SESSION[$sessionKey . '_time'] ?? 0;
// 過期檢查
if (time() - $time > 600) return false;
// 一次性驗證 (驗證後立即銷毀)
$valid = $stored !== '' && strtolower($input) === $stored;
if ($valid) {
unset($_SESSION[$sessionKey], $_SESSION[$sessionKey . '_time']);
}
return $valid;
}
// 無障礙: 語音驗證碼接口
public function audio(): string {
session_start();
$code = $_SESSION[$this->config['sessionKey']] ?? '';
if (!$code) return '';
// 簡單實現: 數字/字母逐個發音 (需 TTS 服務)
// 這裡返回 Base64 編碼的 WAV 數據 (示例)
// 實際項目建議接入百度/騰訊/阿里雲 TTS API
return base64_encode($this->generateAudioWave($code));
}
private function generateAudioWave(string $text): string {
// 生成簡單正弦波 WAV (教學用,實際不可用)
// 實際項目請使用專業 TTS
return '';
}
}
__DIR__ . '/fonts/DejaVuSans-Bold.ttf',
]);
$captcha->generate();
// 2. 前端頁面
?>
聽不清?語音驗證
audio());
| 威脅 | 防禦措施 |
|---|---|
| OCR 識別 | 貝塞爾曲線干擾、字符旋转/扭曲、顏色干擾、字符粘連 |
| Session 劫持 | HTTPS 傳輸、Session ID 定期重生成、HttpOnly Cookie |
| 暴力嘗試 | 失敗計數鎖定 (Redis/IP+UA)、驗證碼一次性銷毀 |
| 繞過驗證 | 服務端強制驗證、Token 機制雙重保障 |
| 自動化腳本 | 行為分析 (滑軌/點擊/時間)、設備指紋、風控引擎 |
| 方案 | 安全性 | 用戶體驗 | 實現成本 | 適用場景 |
|---|---|---|---|---|
| 圖形驗證碼 | 中 | 差 | 低 | 低風險表單、內部系統 |
| 滑塊/點選驗證 | 高 | 好 | 中 (需第三方 SDK) | 登錄/註冊/關鍵操作 |
| 短信/郵箱驗證碼 | 高 | 中 | 高 (通訊費用) | 實名認證/支付/找回密碼 |
| 行為驗證 (無感) | 極高 | 極好 | 高 (需風控平台) | 高價值業務/大規模防刷 |
| 設備指紋 + 風控 | 極高 | 無感 | 極高 | 金融/電商核心交易 |
圖形驗證碼雖非完美,但在成本與安全平衡點上仍是中小項目主流選擇。正確實現 GD 進階繪圖、一次性驗證、無障礙支援,能將破解成本提升 100 倍以上。