示例

服务类 app\common\lib\captcha
<?php
namespace app\common\lib\captcha;use think\facade\Cache;
use think\facade\Config;
use Exception;class Captcha
{private $im = null; private $color = null; protected $config = ['length' => 4, 'fontSize' => 25, 'imageH' => 0, 'imageW' => 0, 'useCurve' => true, 'useNoise' => false, 'bg' => [243, 251, 254], 'fontttf' => '', 'useZh' => false, 'math' => false, 'alpha' => 0, 'api' => false, 'fontPath' => '', 'bgPath' => '', 'expire' => 1800, ];protected $codeSet = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';public function __construct(array $config = []){$this->config = array_merge($this->config, Config::get('captcha', []), $config);if (empty($this->config['fontPath'])) {$this->config['fontPath'] = __DIR__ . '/ttfs/';}}public function create(string $uniqueId = ''): string{$this->clearExpiredCaptchas();if (empty($uniqueId)) {$uniqueId = uniqid('captcha_');}$generator = $this->generate($uniqueId);$this->config['imageW'] = $this->config['imageW'] ?: $this->config['length'] * $this->config['fontSize'] * 1.5;$this->config['imageH'] = $this->config['imageH'] ?: $this->config['fontSize'] * 2;$this->im = imagecreate((int)$this->config['imageW'], (int)$this->config['imageH']);$bgColor = imagecolorallocate($this->im,$this->config['bg'][0],$this->config['bg'][1],$this->config['bg'][2]);imagefill($this->im, 0, 0, $bgColor);$this->color = imagecolorallocate($this->im, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100));if ($this->config['useCurve']) {$this->writeCurve();}$text = str_split($generator['value']);$space = $this->config['imageW'] / $this->config['length'];foreach ($text as $index => $char) {$x = $space * $index + mt_rand(5, 10);$y = $this->config['imageH'] / 2 + $this->config['fontSize'] / 2;$angle = mt_rand(-15, 15);imagettftext($this->im,(int)$this->config['fontSize'],$angle,(int)$x,(int)$y,$this->color,$this->getFont(),$char);}ob_start();imagepng($this->im);$content = ob_get_clean();imagedestroy($this->im);return $content;}public function check(string $code, string $uniqueId = ''): bool{if (empty($uniqueId)) {return false;}$cacheData = Cache::get($uniqueId);if (!$cacheData || time() - $cacheData['time'] > $this->config['expire']) {$this->removeCaptchaFromRecords($uniqueId);return false;}$result = password_verify(strtoupper($code), $cacheData['key']);return $result;}protected function generate(string $uniqueId): array{$bag = '';$characters = str_split($this->codeSet);for ($i = 0; $i < $this->config['length']; $i++) {$bag .= $characters[random_int(0, count($characters) - 1)];}$key = strtoupper($bag);$hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]);Cache::set($uniqueId, ['key' => $hash,'time' => time(),], $this->config['expire']);$this->addCaptchaToRecords($uniqueId);return ['value' => $bag, 'key' => $hash];}protected function addCaptchaToRecords(string $uniqueId): void{$records = Cache::get('captcha_records', []);$records[$uniqueId] = time() + $this->config['expire'];if (count($records) > 1000) {$records = array_slice($records, -500, null, true);}Cache::set('captcha_records', $records);}protected function removeCaptchaFromRecords(string $uniqueId): void{$records = Cache::get('captcha_records', []);unset($records[$uniqueId]);Cache::set('captcha_records', $records);}protected function clearExpiredCaptchas(): void{$lastClear = Cache::get('last_captcha_clear', 0);if (time() - $lastClear < 3600) {return;}$records = Cache::get('captcha_records', []);$now = time();$cleaned = false;foreach ($records as $uid => $expireTime) {if ($expireTime < $now) {Cache::delete($uid);unset($records[$uid]);$cleaned = true;}}if ($cleaned) {Cache::set('captcha_records', $records);Cache::set('last_captcha_clear', time());}}protected function getFont(): string{if (!empty($this->config['fontttf'])) {return $this->config['fontttf'];}$fonts = glob($this->config['fontPath'] . '*.ttf') +glob($this->config['fontPath'] . '*.otf');if (empty($fonts)) {throw new Exception('验证码字体文件不存在,请检查字体路径: ' . $this->config['fontPath']);}return $fonts[array_rand($fonts)];}protected function writeCurve(): void{$px = $py = 0;$A = mt_rand(1, (int)($this->config['imageH'] / 2)); $b = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4)); $f = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4)); $T = mt_rand($this->config['imageH'], $this->config['imageW'] * 2); $w = (2 * M_PI) / $T;$px1 = 0; $px2 = mt_rand((int)($this->config['imageW'] / 2), (int)($this->config['imageW'] * 0.8)); for ($px = $px1; $px <= $px2; $px++) {if ($w != 0) {$py = $A * sin($w * $px + $f) + $b + $this->config['imageH'] / 2;$i = (int)($this->config['fontSize'] / 5);while ($i > 0) {imagesetpixel($this->im, $px + $i, $py + $i, $this->color);$i--;}}}$A = mt_rand(1, (int)($this->config['imageH'] / 2));$f = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4));$T = mt_rand($this->config['imageH'], $this->config['imageW'] * 2);$w = (2 * M_PI) / $T;$b = $py - $A * sin($w * $px + $f) - $this->config['imageH'] / 2;$px1 = $px2;$px2 = $this->config['imageW'];for ($px = $px1; $px <= $px2; $px++) {if ($w != 0) {$py = $A * sin($w * $px + $f) + $b + $this->config['imageH'] / 2;$i = (int)($this->config['fontSize'] / 5);while ($i > 0) {imagesetpixel($this->im, $px + $i, $py + $i, $this->color);$i--;}}}}
}
控制器调用
public function getCaptcha(){$uid = 'captcha_' . uniqid('', true);$captcha = new \app\common\lib\captcha\Captcha();$img = $captcha->create($uid);return json(['image' => 'data:image/png;base64,'.base64_encode($img),'uid' => $uid]);}public function checkCaptcha(){$code = input('post.code');$uid = input('post.uid');$captcha = new \app\common\lib\captcha\Captcha();$result = $captcha->check($code, $uid);return json(['success' => $result,'input_code' => $code,'uid' => $uid]);}