傳統英數字驗證碼易被 OCR 識別、機器學習破解。算術驗證碼需理解邏輯運算,中文漢字驗證碼字符集大、字型變化豐富,均能大幅提升破解門檻。
<?php
// math_captcha.php
session_start();
header("Content-Type: image/png");
// 1. 產生隨機算式
$num1 = mt_rand(0, 9);
$num2 = mt_rand(1, 9); // 除數不為 0
$ops = ['+', '-', '*'];
$op = $ops[array_rand($ops)];
$type = mt_rand(1, 3); // 顯示模式
switch ($type) {
case 1: // 標準:a + b = ?
$code = "$num1 $op $num2 = ?";
$result = $num1 . $op . $num2;
eval("\$answer = $result;");
$_SESSION['captcha_answer'] = $answer;
break;
case 2: // 反推:a + _ = c
$result = $num1 . $op . $num2;
eval("\$answer = $result;");
$code = "$num1 $op _ = $answer";
$_SESSION['captcha_answer'] = $num2;
break;
case 3: // 反推:_ + b = c
$result = $num1 . $op . $num2;
eval("\$answer = $result;");
$code = "_ $op $num2 = $answer";
$_SESSION['captcha_answer'] = $num1;
break;
}
// 2. 繪製圖片
$img = imagecreate(100, 36);
$bg = imagecolorallocate($img, 245, 245, 245);
$textColor = imagecolorallocate($img, 30, 30, 30);
$noiseColor = imagecolorallocate($img, 180, 180, 180);
$lineColor = imagecolorallocate($img, 200, 200, 200);
imagefill($img, 0, 0, $bg);
// 干擾線
for ($i = 0; $i < 5; $i++) {
imageline($img, mt_rand(0,100), mt_rand(0,36), mt_rand(0,100), mt_rand(0,36), $lineColor);
}
// 干擾點
for ($i = 0; $i < 80; $i++) {
imagesetpixel($img, mt_rand(0,100), mt_rand(0,36), $noiseColor);
}
// 繪製文字(置中)
$fontSize = 5;
$textWidth = imagefontwidth($fontSize) * strlen($code);
$textHeight = imagefontheight($fontSize);
$x = (100 - $textWidth) / 2;
$y = (36 - $textHeight) / 2;
imagestring($img, $fontSize, $x, $y, $code, $textColor);
imagepng($img);
imagedestroy($img);
?>
<?php
// chinese_captcha.php
session_start();
header("Content-Type: image/png");
// 1. 字庫:常用 3000 字
$chStr = "的一是在了不和有大這主中人上為們地個用工時要動國產以我到他會作來分生對於學下級就年階義發成部民可出能方進同行面說種過命度革而多子後自社加小機也經力線本電高量長黨得實家定深法表著水理化爭現所二起政三好十戰無農使性前等反體合斗路圖把結第裡正新開論之物從當兩些還天資事隊批如應形想制心樣干都向變關點育重其思與間內去因件日利相由壓員氣業代全組數果期導平各基或月毛然問比展那它最及外沒看治提五解系林者米群頭意只明四道馬認次文通但條較克又公孔領軍流入接席位情運器並飛原油放立題質指建區驗活眾很教決特此常石強極土少已根共直團統式轉別造切九你取西持總料連任志觀調七麼山程百報更見必真保熱委手改管處己將修支識病象幾先老光專什六型具示復安帶每東增則完風回南廣勞輪科北打積車計給節做務被整聯步類集號列溫裝即毫知軸研單色堅據速防史拉世設達爾場織歷花受求傳口斷況采精金界品判參層止邊清至萬確究書術狀廠須離再目海交權且兒青才證低越際八試規斯近注辦布門鐵需走議縣兵固除般引齒千勝細影濟白格效置推空配刀葉率述今選養德話查差半敵始片施響收華覺備名紅續均藥標記難存測士身緊液派准斤角降維板許破述技消底床田勢端感往神便賀村構照容非搞亞磨族火段算適講按值美態黃易彪服早班麥削信排台聲該擊素張密害侯草何樹肥繼右屬市嚴徑螺檢左頁抗蘇顯苦英快稱壞移約巴材省黑武培著河帝僅針怎植京助升王眼她抓含苗副雜普談圍食射源例致酸舊卻充足短劃劑宣環落首尺波承粉踐府魚隨考刻靠夠滿夫失包住促枝局菌桿周護巖師舉曲春元超負砂封換太模貧減陽揚江析畝木言球朝醫校古呢稻宋聽唯輸滑站另衛字鼓剛寫劉微略范供阿塊某功套友限項余倒卷創律雨讓骨遠幫初皮播優占死毒圈偉季訓控激找叫雲互跟裂糧粒母練塞鋼頂策雙留誤礎吸阻故寸盾晚絲女散焊功株親院冷徹彈錯散商視藝滅版烈零室輕血倍缺厘泵察絕富城沖噴壤簡否柱李望盤磁雄似困鞏益洲脫投送奴側潤蓋揮距觸星松送獲興獨官混紀依未突架寬冬章濕偏紋吃執閥礦寨責熟穩奪硬價努翻奇甲預職評讀背協損棉侵灰雖矛厚羅泥辟告卵箱掌氧恩愛停曾溶營終綱孟錢待盡俄縮沙退陳討奮械載胞幼哪剝迫旋征槽倒握擔仍呀鮮吧卡粗介鑽逐弱腳怕鹽末陰豐編印蜂急拿擴傷飛露核緣游振操央伍域甚迅輝異序免紙夜鄉久隸缸夾念蘭映溝乙嗎儒殺汽磷艱晶插埃燃歡鐵補咱芽永瓦傾陣碳演威附牙芽永瓦斜灌歐獻順豬洋腐請透司危括脈宜笑若尾束壯暴企菜穗楚漢愈綠拖牛份染既秋遍鍛玉夏療尖殖井費州訪吹榮銅沿替滾客召旱悟刺腦措貫藏敢令隙爐殼硫煤迎鑄粘探臨薄旬善福縱擇禮願伏殘雷延煙句純漸耕跑澤慢栽魯赤繁境潮橫掉錐希池敗船假亮謂托伙哲懷割擺貢呈勁財儀沉煉麻罪祖息車穿貨銷齊鼠抽畫飼龍庫守築房歌寒喜哥洗蝕廢納腹乎錄鏡婦惡脂莊擦險贊鐘搖典柄辯竹谷賣亂虛橋奧伯趕垂途額壁網截野遺靜謀弄掛課鎮妄盛耐援扎慮鍵歸符慶聚繞摩忙舞遇索顧膠羊湖釘仁音跡碎伸燈避泛亡答勇頻皇柳哈揭甘諾概憲濃島襲誰洪謝炮澆斑訊懂靈蛋閉孩釋乳巨徒私銀伊景坦累勻霉杜樂勒隔彎績招紹胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗鹼殊崗挖氏刃劇堆赫荷胸衡勤膜篇登駐案刊秧緩凸役剪川雪鏈漁啦臉戶洛孢勃盟買楊宗焦賽旗濾硅炭股坐蒸凝竟陷槍黎救冒暗洞犯筒您宋弧爆謬塗味津臂障褐陸啊健尊豆拔莫抵桑坡縫警挑污冰柬嘴啥飯塑寄趙喊墊康遵牧遭幅園腔訂香肉弟屋敏恢忘衣孫齡嶺騙休借丹渡耳刨虎筆稀昆浪薩茶滴淺擁穴覆倫娘噸浸袖珠雌媽紫戲塔錘震歲貌潔剖牢鋒疑霸閃埔猛訴刷狠忽災鬧喬唐漏聞沈熔氯荒莖男凡搶像漿旁玻亦忠唱蒙予紛捕鎖尤乘烏智淡允叛畜俘摸銹掃畢璃寶芯爺鑒秘淨蔣鈣肩騰枯拋軌堂拌爸循誘祝勵肯酒繩窮塘燥泡袋朗喂鋁軟渠顆慣貿糞綜牆趨彼屆墨礙啟逆卸航霧冠丙街萊貝輻腸付吉滲瑞驚頓擠秒懸姆爛森糖聖凹陶詞遲蠶億矩";
$len = mb_strlen($chStr, 'UTF-8');
$chars = [];
for ($i = 0; $i < 4; $i++) {
$pos = mt_rand(0, $len - 1);
$chars[] = mb_substr($chStr, $pos, 1, 'UTF-8');
}
$answer = implode('', $chars);
$_SESSION['captcha_answer'] = $answer;
// 2. 繪製圖片
$width = 130; $height = 50;
$img = imagecreatetruecolor($width, $height);
// 背景漸層
for ($y = 0; $y < $height; $y++) {
$color = imagecolorallocate($img, 255 - $y/2, 255 - $y/3, 255 - $y/4);
imageline($img, 0, $y, $width, $y, $color);
}
// 干擾線
for ($i = 0; $i < 8; $i++) {
$color = imagecolorallocate($img, mt_rand(100,200), mt_rand(100,200), mt_rand(100,200));
imageline($img, mt_rand(0,$width), mt_rand(0,$height), mt_rand(0,$width), mt_rand(0,$height), $color);
}
// 干擾點
for ($i = 0; $i < 200; $i++) {
$color = imagecolorallocate($img, mt_rand(0,255), mt_rand(0,255), mt_rand(0,255));
imagesetpixel($img, mt_rand(0,$width), mt_rand(0,$height), $color);
}
// 繪製中文(需 TTF 字型檔:simhei.ttf 或微軟雅黑)
$fontFile = __DIR__ . '/fonts/simhei.ttf'; // 請自行下載放置
if (!file_exists($fontFile)) {
die('字型檔不存在:' . $fontFile);
}
for ($i = 0; $i < 4; $i++) {
$size = mt_rand(20, 24);
$angle = mt_rand(-15, 15);
$x = 10 + $i * 28 + mt_rand(-3, 3);
$y = 30 + mt_rand(-5, 5);
$color = imagecolorallocate($img, mt_rand(0,80), mt_rand(0,80), mt_rand(0,80));
imagettftext($img, $size, $angle, $x, $y, $color, $fontFile, $chars[$i]);
}
imagepng($img);
imagedestroy($img);
?>
<form action="verify.php" method="post">
<img src="math_captcha.php" id="captchaImg" onclick="this.src='math_captcha.php?'+Date.now()" style="cursor:pointer;vertical-align:middle">
<input type="text" name="captcha" placeholder="請輸入答案" required>
<button type="submit">提交</button>
</form>
<script>
// 點擊圖片刷新
document.getElementById('captchaImg').onclick = function() {
this.src = this.src.split('?')[0] + '?' + Date.now();
};
</script>
<?php
// verify.php
session_start();
$userInput = trim($_POST['captcha'] ?? '');
$answer = $_SESSION['captcha_answer'] ?? '';
if (strcasecmp($userInput, $answer) === 0) {
echo "驗證通過!";
unset($_SESSION['captcha_answer']); // 一次性使用
} else {
echo "驗證碼錯誤!";
}
?>
| 風險 | 對策 |
|---|---|
| Session 固定攻擊 | session_regenerate_id(true) 登入後重新生成 ID |
| 暴力嘗試 | 限制同 IP/Session 驗證失敗次數,鎖定 15 分鐘 |
| 圖片快取 | Header: Cache-Control: no-store, no-cache, must-revalidate |
| 字型檔缺失 | 部署時確認 TTF 字型路徑,或打包字型檔 |
| 無障礙存取 | 提供音頻驗證碼替代方案 |
自建驗證碼適合學習、簡單專案或無法引入外部服務的環境;生產環境高流量系統建議採用成熟第三方服務。