| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- <?php
- namespace App\Services\Account;
- use App\Cache\CaptchaCache;
- use App\Consts\ErrorConst;
- use App\Exceptions\ApiException;
- use App\Libs\Utils;
- class CaptchaService
- {
- /**
- * 生成验证码:返回随机 key 与 base64 图片
- *
- * 答案只保存在服务端(Redis),返回给前端的仅是一个随机 key。
- *
- * @return array
- */
- public function generate(): array
- {
- $code = $this->randomCode((int)config('captcha.length', 4));
- $key = md5(uniqid((string)mt_rand(), true));
- CaptchaCache::set($key, $code, (int)config('captcha.ttl', 300));
- return [
- 'captcha_key' => $key,
- 'image' => 'data:image/png;base64,' . base64_encode($this->render($code)),
- ];
- }
- /**
- * 校验验证码
- *
- * 无论校验成功或失败,答案都会被消费掉(不复用),避免重放与暴力试错。
- *
- * @param string $key 验证码 key
- * @param string $code 用户输入的验证码
- * @return void
- * @throws ApiException
- */
- public function verify(string $key, string $code): void
- {
- $key = trim($key);
- $code = trim($code);
- if ($key === '' || $code === '') {
- Utils::throwError(ErrorConst::CAPTCHA_VERIFY_ERROR);
- }
- // 一次性取出,不存在或已过期都返回空
- $expected = CaptchaCache::pull($key);
- if ($expected === '') {
- Utils::throwError(ErrorConst::CAPTCHA_VERIFY_ERROR);
- }
- $input = config('captcha.case_insensitive', true) ? strtolower($code) : $code;
- if (!hash_equals($expected, $input)) {
- Utils::throwError(ErrorConst::CAPTCHA_VERIFY_ERROR);
- }
- }
- /**
- * 生成随机验证码
- *
- * @param int $length
- * @return string
- */
- private function randomCode(int $length): string
- {
- $charset = (string)config('captcha.charset', 'ABCDEFGHJKMNPQRSTUVWXY3456789');
- $max = strlen($charset) - 1;
- $code = '';
- for ($i = 0; $i < $length; $i++) {
- $code .= $charset[mt_rand(0, $max)];
- }
- return $code;
- }
- /**
- * 渲染验证码图片,返回 PNG 二进制内容
- *
- * 线上 GD 未启用 FreeType,imagettftext 不可用,这里使用内置字体 imagestring;
- * 内置字体最大仅 5 号(约 9x15),因此先按 1/2 尺寸绘制再整体放大。
- *
- * @param string $code
- * @return string
- */
- private function render(string $code): string
- {
- $width = max(60, (int)config('captcha.width', 132));
- $height = max(24, (int)config('captcha.height', 48));
- $scale = 2;
- $fw = (int)($width / $scale);
- $fh = (int)($height / $scale);
- $img = imagecreatetruecolor($fw, $fh);
- imagefill($img, 0, 0, imagecolorallocate($img, 245, 247, 250));
- $font = 5;
- $charH = imagefontheight($font);
- $len = strlen($code);
- $step = (int)(($fw - 4) / max(1, $len));
- for ($i = 0; $i < $len; $i++) {
- $color = imagecolorallocate($img, mt_rand(20, 110), mt_rand(20, 110), mt_rand(20, 110));
- $x = 2 + $i * $step + mt_rand(0, 2);
- $y = max(0, (int)(($fh - $charH) / 2) + mt_rand(-3, 3));
- imagestring($img, $font, $x, $y, $code[$i], $color);
- }
- $this->drawNoise($img, $fw, $fh);
- // 放大输出(点阵放大后带锯齿感,对简单 OCR 也有一定干扰)
- $out = imagecreatetruecolor($width, $height);
- imagecopyresized($out, $img, 0, 0, 0, 0, $width, $height, $fw, $fh);
- imagedestroy($img);
- // 放大后再叠加干扰线,弱化点阵规律
- for ($i = 0; $i < 3; $i++) {
- imageline(
- $out,
- mt_rand(0, $width),
- mt_rand(0, $height),
- mt_rand(0, $width),
- mt_rand(0, $height),
- imagecolorallocate($out, mt_rand(150, 215), mt_rand(150, 215), mt_rand(150, 215))
- );
- }
- ob_start();
- imagepng($out);
- imagedestroy($out);
- return (string)ob_get_clean();
- }
- /**
- * 绘制干扰线与噪点
- *
- * @param resource $img
- * @param int $w
- * @param int $h
- * @return void
- */
- private function drawNoise($img, int $w, int $h): void
- {
- for ($i = 0; $i < 4; $i++) {
- imageline(
- $img,
- mt_rand(0, $w),
- mt_rand(0, $h),
- mt_rand(0, $w),
- mt_rand(0, $h),
- imagecolorallocate($img, mt_rand(160, 220), mt_rand(160, 220), mt_rand(160, 220))
- );
- }
- for ($i = 0; $i < 60; $i++) {
- imagesetpixel(
- $img,
- mt_rand(0, max(0, $w - 1)),
- mt_rand(0, max(0, $h - 1)),
- imagecolorallocate($img, mt_rand(120, 200), mt_rand(120, 200), mt_rand(120, 200))
- );
- }
- }
- }
|