139 lines
4.3 KiB
PHP
139 lines
4.3 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ywxapp\service;
|
||
|
||
use think\facade\Cache;
|
||
|
||
/**
|
||
* 图形验证码服务(API 友好,基于 GD + Cache,无需 session)
|
||
*
|
||
* 设计要点:
|
||
* - 生成时返回 {token, image(base64), expire},前端把 image 直接塞进 <img src>,
|
||
* 提交时回传 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);
|
||
}
|
||
}
|