mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-23 10:34:25 +08:00
更新后端基础
This commit is contained in:
143
app/services/AuthService.php
Normal file
143
app/services/AuthService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
101
app/services/CaptchaService.php
Normal file
101
app/services/CaptchaService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
43
app/services/UserService.php
Normal file
43
app/services/UserService.php
Normal 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' => ['*:*:*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user