// +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\service; use think\facade\Cache; /** * 图形验证码服务(API 友好,基于 GD + Cache,无需 session) * * 设计要点: * - 生成时返回 {token, image(base64), expire},前端把 image 直接塞进 , * 提交时回传 token + 用户输入的 captcha;服务端比对 Cache 中的 token 值。 * - 与 think-captcha 的 session 方案不同,本服务完全无状态,适配小程序 / APP / SPA 等场景。 * - 验证码一次性(校验成功后立即失效),有效期 120s。 * * @package ywxapp\service */ class CaptchaService { const PREFIX = 'captcha_'; const EXPIRE = 120; public static function instance(): self { return new self(); } /** * 生成图形验证码 * * @param int $length 字符数 * @return array ['token'=>string, 'image'=>string(base64 data uri), 'expire'=>int] * @throws \Exception */ public function generate(int $length = 4): array { if (!extension_loaded('gd')) { throw new \Exception('服务器未启用 GD 扩展,无法生成图形验证码'); } $code = $this->randomCode($length); $token = bin2hex(random_bytes(16)); Cache::set(self::PREFIX . $token, strtolower($code), self::EXPIRE); return [ 'token' => $token, 'image' => $this->renderImage($code), 'expire' => self::EXPIRE, ]; } /** * 校验图形验证码(一次性) * * @param string $token 生成时返回的 token * @param string $code 用户输入 * @return bool */ public function verify(string $token, string $code): bool { if ($token === '' || $code === '') { return false; } $key = self::PREFIX . $token; $stored = Cache::get($key); if ($stored === null) { return false; } $ok = strtolower(trim($code)) === strtolower((string) $stored); if ($ok) { Cache::delete($key); } return $ok; } /** * 生成随机可读字符(去除易混淆的 0/O/1/I/L) */ protected function randomCode(int $length): string { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $max = strlen($chars) - 1; $code = ''; for ($i = 0; $i < $length; $i++) { $code .= $chars[mt_rand(0, $max)]; } return $code; } /** * 用 GD 绘制验证码 PNG,返回 data URI */ protected function renderImage(string $code): string { $w = 120; $h = 44; $img = imagecreatetruecolor($w, $h); // 背景 $bg = imagecolorallocate($img, 240, 242, 245); imagefill($img, 0, 0, $bg); // 干扰点 for ($i = 0; $i < 80; $i++) { $c = imagecolorallocate($img, mt_rand(170, 230), mt_rand(170, 230), mt_rand(170, 230)); imagesetpixel($img, mt_rand(0, $w - 1), mt_rand(0, $h - 1), $c); } // 干扰线 for ($i = 0; $i < 3; $i++) { $c = imagecolorallocate($img, mt_rand(150, 210), mt_rand(150, 210), mt_rand(150, 210)); imageline($img, mt_rand(0, $w), mt_rand(0, $h), mt_rand(0, $w), mt_rand(0, $h), $c); } // 字符 $len = strlen($code); for ($i = 0; $i < $len; $i++) { $color = imagecolorallocate($img, mt_rand(30, 130), mt_rand(30, 130), mt_rand(30, 130)); $size = mt_rand(5, 5); // imagechar 内置字体大小(1-5) $x = (int) (10 + $i * ($w / $len)); $y = mt_rand(8, 16); imagechar($img, $size, $x, $y, $code[$i], $color); } ob_start(); imagepng($img); $raw = ob_get_clean(); imagedestroy($img); return 'data:image/png;base64,' . base64_encode($raw); } }