138 lines
3.6 KiB
PHP
138 lines
3.6 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use Illuminate\Support\Str;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
/**
|
||
* 后台登录图形验证码:SVG 产出 + Cache 短时保存答案摘要(单行文本,便于前台用 img[src=data:...] 展示)。
|
||
*/
|
||
final class AdminCaptchaService
|
||
{
|
||
private const PREFIX = 'admin_captcha:';
|
||
|
||
private const TTL_SECONDS = 120;
|
||
|
||
/** 剔除易混淆字符 */
|
||
private const CHARSET = '23456789abcdefghjkmpqrstuvwxyz';
|
||
|
||
/**
|
||
* @return array{captcha_key: string, image_svg: string, image_base64: string}
|
||
*/
|
||
public function create(): array
|
||
{
|
||
$code = $this->randomCode();
|
||
$key = (string) Str::uuid();
|
||
|
||
Cache::put(
|
||
self::PREFIX.$key,
|
||
$this->digest($code),
|
||
now()->addSeconds(self::TTL_SECONDS),
|
||
);
|
||
|
||
$svg = $this->renderSvg($code);
|
||
|
||
return [
|
||
'captcha_key' => $key,
|
||
'image_svg' => $svg,
|
||
'image_base64' => base64_encode($svg),
|
||
];
|
||
}
|
||
|
||
public function verify(?string $captchaKey, ?string $captchaInput): bool
|
||
{
|
||
if ($captchaKey === null || $captchaKey === ''
|
||
|| $captchaInput === null || trim($captchaInput) === '') {
|
||
return false;
|
||
}
|
||
|
||
$digest = Cache::pull(self::PREFIX.$captchaKey);
|
||
if ($digest === null) {
|
||
return false;
|
||
}
|
||
|
||
$guess = strtolower(trim($captchaInput));
|
||
|
||
return hash_equals($digest, $this->digest($guess));
|
||
}
|
||
|
||
private function digest(string $normalizedCode): string
|
||
{
|
||
return hash_hmac(
|
||
'sha256',
|
||
$normalizedCode,
|
||
(string) config('app.key'),
|
||
);
|
||
}
|
||
|
||
private function randomCode(int $length = 4): string
|
||
{
|
||
$out = '';
|
||
$max = strlen(self::CHARSET) - 1;
|
||
for ($i = 0; $i < $length; $i++) {
|
||
$out .= self::CHARSET[random_int(0, $max)];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
private function renderSvg(string $code): string
|
||
{
|
||
$w = 160;
|
||
$h = 48;
|
||
$bg = '#fafafa';
|
||
|
||
$escape = static fn (string $c): string => htmlspecialchars($c, ENT_XML1 | ENT_QUOTES);
|
||
|
||
$lines = '';
|
||
for ($i = 0; $i < 4; $i++) {
|
||
$x1 = random_int(0, $w);
|
||
$y1 = random_int(0, $h);
|
||
$x2 = random_int(0, $w);
|
||
$y2 = random_int(0, $h);
|
||
$stroke = sprintf('#%06x', random_int(0x9_00_000, 0xBF_BF_BF));
|
||
$lines .= sprintf(
|
||
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="1" opacity="0.35"/>',
|
||
$x1,
|
||
$y1,
|
||
$x2,
|
||
$y2,
|
||
$stroke,
|
||
);
|
||
}
|
||
|
||
$chars = str_split($code);
|
||
$texts = '';
|
||
$x = 18;
|
||
foreach ($chars as $ch) {
|
||
$rot = random_int(-20, 20);
|
||
$dy = random_int(-3, 3);
|
||
$y = 32 + $dy;
|
||
$fill = sprintf('#%06x', random_int(0x20_20_20, 0x3F_3F_3F));
|
||
$texts .= sprintf(
|
||
'<text x="%d" y="%d" font-family="ui-monospace,monospace" font-size="24" font-weight="600" fill="%s" transform="rotate(%d %d %d)">%s</text>',
|
||
$x,
|
||
$y,
|
||
$fill,
|
||
$rot,
|
||
$x,
|
||
$y,
|
||
$escape($ch),
|
||
);
|
||
$x += 34;
|
||
}
|
||
|
||
return sprintf(
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d"><rect width="100%%" height="100%%" fill="%s"/>%s%s</svg>',
|
||
$w,
|
||
$h,
|
||
$w,
|
||
$h,
|
||
$bg,
|
||
$lines,
|
||
$texts,
|
||
);
|
||
}
|
||
}
|