示例

服务类 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
]);
}