Files

139 lines
4.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}