feat: 添加 Laravel Sanctum 支持,增强管理员 API 鉴权,更新相关中间件与路由配置

This commit is contained in:
2026-05-09 11:11:46 +08:00
parent e478597d13
commit 8a70c029f6
20 changed files with 717 additions and 14 deletions

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* 后台登录图形验证码SVG 产出 + Cache 短时保存答案摘要(单行文本,便于前台用 img[src=data:...] 展示)。
*/
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,
);
}
}