傳統驗證碼易被 OCR 識別。本文實現一款**高強度驗證碼類**:字符隨機旋轉、傾斜、黏連重疊、正弦波干擾線、多字體大小顏色、自適應畫布。核心難點在於**旋轉後字符邊界計算**與**黏連佈局**,附完整類代碼、字體文件說明、Demo 調用示例。
<?php
class Utils_Caption {
public int $Width = 60; // 最小寬度
public int $Height = 30; // 最小高度
public int $Length = 4; // 驗證碼位數
public string $BgColor = '#FFFFFF';
public array $TFonts = ['font.ttf']; // 字體文件 (需放同目錄 font/ 下)
public array $TFontSize = [17, 20]; // 字體大小範圍
public array $TFontAngle = [-20, 20]; // 旋轉角度範圍
public string $Chars = '0123456789'; // 字符集 (可加字母)
public array $Code = []; // 生成的驗證碼數組 [size, angle, color, char, font, bounds]
public string $Image = '';
public array $FontColors = ['#f36161', '#6bc146', '#5368bd'];
public float $TPadden = 0.75; // 字符間距係數 (黏連關鍵)
public int $Txbase = 5; // 左邊距
public int $Tybase = 5; // 上邊距 (內部由 bounds 計算)
public bool $TLine = true; // 是否畫干擾線
// 生成驗證碼數據 (不輸出圖片)
public function RandRSI(): string {
$this->TFontAngle = range($this->TFontAngle[0], $this->TFontAngle[1]);
$this->TFontSize = range($this->TFontSize[0], $this->TFontSize[1]);
$fontFile = dirname(__FILE__) . '/font/' . $this->TFonts[0];
$charlen = strlen($this->Chars) - 1;
$codeStr = '';
for ($i = 0; $i < $this->Length; $i++) {
$char = $this->Chars[random_int(0, $charlen)];
$angle = $this->TFontAngle[random_int(0, count($this->TFontAngle)-1)];
$size = $this->TFontSize[random_int(0, count($this->TFontSize)-1)];
$color = $this->FontColors[random_int(0, count($this->FontColors)-1)];
$bounds = $this->_calculateTextBox($size, $angle, $fontFile, $char);
$this->Code[] = [$size, $angle, $color, $char, $fontFile, $bounds];
$codeStr .= $char;
}
return $codeStr;
}
// 繪製並輸出 PNG
public function Draw(): void {
if (empty($this->Code)) $this->RandRSI();
[$width, $height] = $this->_getImageWH($this->Code);
$this->Width = $width;
$this->Height = $height;
$img = imagecreatetruecolor($width, $height); // 真彩色
$bg = $this->_getColor2($this->_getColor($this->BgColor));
imagefilledrectangle($img, 0, 0, $width, $height, $bg);
$x = $this->Txbase;
foreach ($this->Code as $v) {
[$size, $angle, $color, $char, $font, $bounds] = $v;
$c = $this->_getColor2($this->_getColor($color));
// imagettftext 基線在字符底部,y = 基線 = 上邊距 + bounds['height']
imagettftext($img, $size, $angle, $x, $bounds['height'], $c, $font, $char);
// 下一字符 x: 當前 x + 字符寬度*間距係數 - 左側溢出
$x += $bounds['width'] * $this->TPadden - $bounds['left'];
}
if ($this->TLine) $this->_writeSinLine($img, $x);
header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);
}
// 計算字符旋轉後的外包矩形
private function _calculateTextBox(int $size, int $angle, string $font, string $char): array {
$box = imagettfbbox($size, $angle, $font, $char);
$minX = min($box[0], $box[2], $box[4], $box[6]);
$maxX = max($box[0], $box[2], $box[4], $box[6]);
$minY = min($box[1], $box[3], $box[5], $box[7]);
$maxY = max($box[1], $box[3], $box[5], $box[7]);
return [
'left' => ($minX >= -1) ? -abs($minX + 1) : abs($minX + 2),
'top' => abs($minY),
'width' => $maxX - $minX,
'height' => $maxY - $minY,
'box' => $box,
];
}
private function _getColor(string $hex): array {
return [hexdec($hex[1].$hex[2]), hexdec($hex[3].$hex[4]), hexdec($hex[5].$hex[6])];
}
private function _getColor2(array $rgb): int {
return imagecolorallocate($this->Image, $rgb[0], $rgb[1], $rgb[2]);
}
// 計算畫布寬高
private function _getImageWH(array $codes): array {
$w = $this->Txbase;
$h = 0;
foreach ($codes as $v) {
$w += $v[5]['width'] * $this->TPadden - $v[5]['left'];
$h = max($h, $v[5]['height']);
}
return [max($w, $this->Width), max($h, $this->Height)];
}
// 正弦波干擾線
private function _writeSinLine($img, float $width): void {
$h = $this->Height;
$h1 = random_int(-5, 5); // 垂直偏移
$h2 = random_int(-1, 1); // 雙線偏移
$w2 = random_int(10, 15); // 頻率分母
$h3 = random_int(4, 6); // 振幅分母
$color = $this->_getColor2($this->_getColor($this->FontColors[array_rand($this->FontColors)]));
for ($i = -$width/2; $i < $width/2; $i += 0.1) {
$y = $h/$h3 * sin($i/$w2) + $h/2 + $h1;
imagesetpixel($img, $i + $width/2, $y, $color);
if ($h2 != 0) imagesetpixel($img, $i + $width/2, $y + $h2, $color);
}
}
}
?>
<?php
require_once 'Utils_Caption.php';
session_start();
$captcha = new Utils_Caption();
$captcha->TFontSize = [15, 17];
$captcha->Width = 50;
$captcha->Height = 25;
$code = $captcha->RandRSI();
$_SESSION['CHECKCODE'] = $code; // 存入 Session 驗證
$captcha->Draw(); // 直接輸出圖片
?>
<form action="check.php" method="post">
驗證碼: <input name="code" type="text" maxlength="4">
<img src="captcha.php" onclick="this.src='captcha.php?'+Math.random()" style="cursor:pointer" title="點擊刷新">
<button type="submit">提交</button>
</form>
<?php
// check.php
session_start();
if (strtoupper($_POST['code']) === strtoupper($_SESSION['CHECKCODE'])) {
echo '驗證通過';
} else {
echo '驗證碼錯誤';
}
?>
| 措施 | 實現方式 |
|---|---|
| 字符集擴大 | `$Chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'` (去除易混淆 0/O/1/I) |
| 增加干擾點/弧線 | 在 `_writeSinLine` 後添加 `imageellipse`、`imagesetpixel` 隨機噪點 |
| 背景圖片/漸變 | 用 `imagecreatefromjpeg` 載入背景圖,或 `imagefilledpolygon` 畫漸變多邊形 |
| 限制請求頻率 | 同 IP 分鐘請求 > 10 次返回 429,防暴力刷新 |
| Session 驗證碼一次性 | 驗證成功後 `unset($_SESSION['CHECKCODE'])` |
高安全場景 (登錄、支付、投票) 建議接入**行為驗證/滑塊驗證/點選驗證** (Geetest、hCaptcha、Turnstile、阿里云驗證碼),前端交互友好、機器識別極難、支持風控模型,避免自研維護成本。