更新后端基础

This commit is contained in:
技术老胡
2026-02-23 11:33:27 +08:00
parent 4a34feec54
commit d29751cce8
75 changed files with 2978 additions and 1489 deletions

View File

@@ -0,0 +1,143 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use app\common\utils\JwtUtil;
use app\repositories\UserRepository;
use support\Cache;
/**
* 认证服务
*
* 处理登录、token 生成等认证相关业务
*/
class AuthService extends BaseService
{
public function __construct(
protected UserRepository $userRepository,
protected CaptchaService $captchaService
) {
}
/**
* 用户登录
*
* 登录成功后返回 token前端使用该 token 通过 Authorization 请求头访问需要认证的接口
*
* @param string $username 用户名
* @param string $password 密码
* @param string $verifyCode 验证码
* @param string $captchaId 验证码ID
* @return array ['token' => string]
* @throws \RuntimeException
*/
public function login(string $username, string $password, string $verifyCode, string $captchaId): array
{
// 1. 校验验证码
if (!$this->captchaService->validate($captchaId, $verifyCode)) {
throw new \RuntimeException('验证码错误或已失效', 400);
}
// 2. 查询用户
$user = $this->userRepository->findByUserName($username);
if (!$user) {
throw new \RuntimeException('账号或密码错误', 401);
}
// 3. 校验密码
if (!$this->validatePassword($password, $user->password)) {
throw new \RuntimeException('账号或密码错误', 401);
}
// 4. 检查用户状态
if ($user->status !== 1) {
throw new \RuntimeException('账号已被禁用', 403);
}
// 5. 生成 JWT token包含用户ID、用户名、昵称等信息
$token = $this->generateToken($user);
// 6. 将 token 信息存入 Redis用于后续刷新、黑名单等
$this->cacheToken($token, $user->id);
// 7. 更新用户最后登录信息
$this->updateLoginInfo($user);
// 返回 token前端使用该 token 访问需要认证的接口
return [
'token' => $token,
];
}
/**
* 校验密码
*
* @param string $password 明文密码
* @param string|null $hash 数据库中的密码hash
* @return bool
*/
private function validatePassword(string $password, ?string $hash): bool
{
// 如果数据库密码为空,允许使用默认密码(仅用于开发/演示)
if ($hash === null || $hash === '') {
// 开发环境:允许 admin/123456 和 common/123456 无密码登录
// 生产环境应移除此逻辑
return in_array($password, ['123456'], true);
}
return password_verify($password, $hash);
}
/**
* 生成 JWT token
*
* @param \app\models\User $user
* @return string
*/
private function generateToken($user): string
{
$payload = [
'user_id' => $user->id,
'user_name' => $user->user_name,
'nick_name' => $user->nick_name,
];
return JwtUtil::generateToken($payload);
}
/**
* 将 token 信息缓存到 Redis
*
* @param string $token
* @param int $userId
*/
private function cacheToken(string $token, int $userId): void
{
$key = JwtUtil::getCachePrefix() . $token;
$data = [
'user_id' => $userId,
'created_at' => time(),
];
Cache::set($key, $data, JwtUtil::getTtl());
}
/**
* 更新用户登录信息
*
* @param \app\models\User $user
*/
private function updateLoginInfo($user): void
{
// 获取客户端真实IP优先使用 x-real-ip其次 x-forwarded-for最后 remoteIp
$request = request();
$ip = $request->header('x-real-ip', '')
?: ($request->header('x-forwarded-for', '') ? explode(',', $request->header('x-forwarded-for', ''))[0] : '')
?: $request->getRemoteIp();
$user->login_ip = trim($ip);
$user->login_at = date('Y-m-d H:i:s');
$user->save();
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use support\Cache;
use Webman\Captcha\CaptchaBuilder;
/**
* 验证码服务
*
* 使用 Redis 缓存验证码信息,支持防重放和错误次数限制
*/
class CaptchaService extends BaseService
{
private const CACHE_PREFIX = 'captcha_';
private const EXPIRE_SECONDS = 300; // 5 分钟
private const MAX_ERROR_TIMES = 5;
/**
* 生成验证码,返回 captchaId 和 base64 图片
*
* @return array ['captchaId' => string, 'image' => string]
*/
public function generate(): array
{
// 使用 webman/captcha 生成验证码图片和文本
$builder = new CaptchaBuilder;
// 适配前端登录表单尺寸110x30
$builder->build(110, 30);
$code = strtolower($builder->getPhrase());
$id = bin2hex(random_bytes(16));
$payload = [
'code' => $code,
'created_at' => time(),
'error_times' => 0,
'used' => false,
];
Cache::set($this->buildKey($id), $payload, self::EXPIRE_SECONDS);
// 获取图片二进制并转为 base64
$imgContent = $builder->get();
$base64 = base64_encode($imgContent ?: '');
return [
'captchaId' => $id,
'image' => 'data:image/jpeg;base64,' . $base64,
];
}
/**
* 校验验证码(基于 captchaId + code
*
* @param string|null $id 验证码ID
* @param string|null $code 用户输入的验证码
* @return bool
*/
public function validate(?string $id, ?string $code): bool
{
if ($id === null || $id === '' || $code === null || $code === '') {
return false;
}
$key = $this->buildKey($id);
$data = Cache::get($key);
if (!$data || !is_array($data)) {
return false;
}
// 已使用或错误次数过多
if (!empty($data['used']) || ($data['error_times'] ?? 0) >= self::MAX_ERROR_TIMES) {
Cache::delete($key);
return false;
}
$expect = (string)($data['code'] ?? '');
if ($expect === '' || strtolower($code) !== strtolower($expect)) {
$data['error_times'] = ($data['error_times'] ?? 0) + 1;
Cache::set($key, $data, self::EXPIRE_SECONDS);
return false;
}
// 标记为已使用,防重放
$data['used'] = true;
Cache::set($key, $data, self::EXPIRE_SECONDS);
return true;
}
/**
* 构建缓存键
*/
private function buildKey(string $id): string
{
return self::CACHE_PREFIX . $id;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use app\common\constants\RoleCode;
use app\repositories\UserRepository;
/**
* 用户相关业务服务示例
*/
class UserService extends BaseService
{
public function __construct(
protected UserRepository $users
) {}
/**
* 根据 ID 获取用户信息(附带角色与权限)
*
* 返回结构尽量与前端 mock 的 /user/getUserInfo 保持一致:
* {
* "user": {...}, // 用户信息roles 字段为角色对象数组
* "roles": ["admin"], // 角色 code 数组
* "permissions": ["*:*:*"] // 权限标识数组
* }
*/
public function getUserInfoById(int $id): array
{
$user = $this->users->find($id);
if (!$user) {
throw new \RuntimeException('用户不存在', 404);
}
$userArray = $user->toArray();
return [
'user' => $userArray,
'roles' => ['admin'],
'permissions' => ['*:*:*'],
];
}
}

View File

@@ -1,104 +0,0 @@
<?php
namespace app\services\auth;
use app\common\base\BaseService;
use app\exceptions\AuthFailedException;
use app\exceptions\UnauthorizedException;
use app\repositories\AdminUserRepository;
use support\Redis;
class AuthService extends BaseService
{
public function __construct(
protected AdminUserRepository $userRepository
) {
}
/**
* 登录,返回 JWT token
*/
public function login(string $username, string $password, $verifyCode = null): string
{
$user = $this->userRepository->findByUsername($username);
if (!$user) {
throw new AuthFailedException();
}
// 当前阶段使用明文模拟密码校验
if ($password !== '123456') {
throw new AuthFailedException();
}
$payload = [
'uid' => $user['id'],
'username' => $user['userName'],
'roles' => $user['roles'] ?? [],
];
$token = JwtService::generateToken($payload);
// 写入 Redis 会话key 使用 token方便快速失效
$ttl = JwtService::getTtl();
$key = 'mpay:auth:token:' . $token;
$redis = Redis::connection('default');
$redis->setex($key, $ttl, json_encode($payload, JSON_UNESCAPED_UNICODE));
return $token;
}
/**
* 根据 token 获取用户信息(对齐前端 mock 返回结构)
*/
public function getUserInfo(string $token, $id = null): array
{
if ($token === '') {
throw new UnauthorizedException();
}
$redis = Redis::connection('default');
$key = 'mpay:auth:token:' . $token;
$session = $redis->get($key);
if (!$session) {
// 尝试从 JWT 解出(例如服务重启后 Redis 丢失的情况)
$payload = JwtService::parseToken($token);
} else {
$payload = json_decode($session, true) ?: [];
}
if (empty($payload['uid']) && empty($payload['username'])) {
throw new UnauthorizedException();
}
// 对齐 mock如果有 id 参数则按 id 查,否则用 payload 中 uid 查
if ($id !== null && $id !== '') {
$user = $this->userRepository->findById((int)$id);
} else {
$user = $this->userRepository->findById((int)($payload['uid'] ?? 0));
}
if (!$user) {
throw new UnauthorizedException();
}
// 角色信息
$roleInfo = $this->userRepository->getRoleInfoByCodes($user['roles'] ?? []);
$user['roles'] = $roleInfo;
$roleCodes = array_map(static fn($item) => $item['code'], $roleInfo);
// 权限信息
if (in_array('admin', $roleCodes, true)) {
$permissions = ['*:*:*'];
} else {
$permissions = $this->userRepository->getPermissionsByRoleCodes($roleCodes);
}
return [
'user' => $user,
'roles' => $roleCodes,
'permissions' => $permissions,
];
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace app\services\auth;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService
{
/**
* 生成 JWT
*/
public static function generateToken(array $payloadBase): string
{
$config = config('jwt', []);
$secret = $config['secret'] ?? 'mpay-secret';
$ttl = (int)($config['ttl'] ?? 7200);
$alg = $config['alg'] ?? 'HS256';
$now = time();
$payload = array_merge($payloadBase, [
'iat' => $now,
'exp' => $now + $ttl,
]);
return JWT::encode($payload, $secret, $alg);
}
/**
* 解析 JWT
*/
public static function parseToken(string $token): array
{
$config = config('jwt', []);
$secret = $config['secret'] ?? 'mpay-secret';
$alg = $config['alg'] ?? 'HS256';
$decoded = JWT::decode($token, new Key($secret, $alg));
return json_decode(json_encode($decoded, JSON_UNESCAPED_UNICODE), true) ?: [];
}
/**
* 获取 ttl
*/
public static function getTtl(): int
{
$config = config('jwt', []);
return (int)($config['ttl'] ?? 7200);
}
}