优化登录接口以及中间件

This commit is contained in:
2026-03-05 16:20:18 +08:00
parent e5f83846b3
commit 39955a17a8
12 changed files with 268 additions and 516 deletions

View File

@@ -33,78 +33,6 @@ class UserLogic
}
}
/**
* 登录:手机号 + 密码,返回用户信息与 user-token并写入 Redis 缓存
*/
public function login(string $phone, string $password): array
{
self::validatePhone($phone);
$user = DicePlayer::where('phone', $phone)->find();
if (!$user) {
throw new ApiException('手机号未注册');
}
if ((int) $user->status !== self::STATUS_NORMAL) {
throw new ApiException('账号已被禁用');
}
$hashed = $this->hashPassword($password);
if ($user->password !== $hashed) {
throw new ApiException('密码错误');
}
$userArr = $user->hidden(['password'])->toArray();
UserCache::setUser((int) $user->id, $userArr);
$userToken = $this->generateUserToken((int) $user->id);
// 同一用户只保留最新一次登录的 token旧 token 自动失效
UserCache::setCurrentUserToken((int) $user->id, $userToken);
return [
'user' => $userArr,
'user-token' => $userToken,
'user_id' => (int) $user->id,
];
}
/**
* 注册:手机号 + 密码(+60创建玩家并返回用户信息与 user-token写入 Redis
*/
public function register(string $phone, string $password, ?string $nickname = null): array
{
self::validatePhone($phone);
if (strlen($password) < 6) {
throw new ApiException('密码至少 6 位');
}
$exists = DicePlayer::where('phone', $phone)->find();
if ($exists) {
throw new ApiException('该手机号已注册');
}
$user = new DicePlayer();
$user->phone = $phone;
$user->username = $phone;
if ($nickname !== null && $nickname !== '') {
$user->name = $nickname;
}
// name 未传时由 DicePlayer::onBeforeInsert 默认设为 uid
$user->password = $this->hashPassword($password);
$user->status = self::STATUS_NORMAL;
$user->save();
$userArr = $user->hidden(['password'])->toArray();
UserCache::setUser((int) $user->id, $userArr);
$userToken = $this->generateUserToken((int) $user->id);
// 同一用户只保留最新一次登录的 token旧 token 自动失效
UserCache::setCurrentUserToken((int) $user->id, $userToken);
return [
'user' => $userArr,
'user-token' => $userToken,
'user_id' => (int) $user->id,
];
}
/**
* 与 DicePlayerLogic 一致的密码加密md5(salt . password)
*/
@@ -114,97 +42,83 @@ class UserLogic
}
/**
* 生成 user-tokenJWTplat=api_userid=user_id
* 登录JSONusername, password, lang, coin, time
* 存在则校验密码并更新 coin累加不存在则创建用户并写入 coin。
* 将会话写入 Redis返回 token 与前端连接地址。
*/
private function generateUserToken(int $userId): string
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time): array
{
$exp = config('api.user_token_exp', 604800);
$result = JwtToken::generateToken([
'id' => $userId,
'plat' => 'api_user',
$username = trim($username);
if ($username === '') {
throw new ApiException('username 不能为空');
}
$player = DicePlayer::where('username', $username)->find();
if ($player) {
$hashed = $this->hashPassword($password);
if ($player->password !== $hashed) {
throw new ApiException('密码错误');
}
$currentCoin = (float) $player->coin;
$player->coin = $currentCoin + $coin;
$player->save();
} else {
$player = new DicePlayer();
$player->username = $username;
$player->phone = $username;
$player->password = $this->hashPassword($password);
$player->status = self::STATUS_NORMAL;
$player->coin = $coin;
$player->save();
}
$exp = (int) config('api.session_expire', 604800);
$tokenResult = JwtToken::generateToken([
'id' => (int) $player->id,
'username' => $username,
'plat' => 'api_login',
'access_exp' => $exp,
]);
return $result['access_token'];
$token = $tokenResult['access_token'];
UserCache::setSessionByUsername($username, $token);
$userArr = $player->hidden(['password'])->toArray();
UserCache::setUser((int) $player->id, $userArr);
$baseUrl = rtrim(config('api.login_url_base', 'https://127.0.0.1:6777'), '/');
$lang = in_array($lang, ['chs', 'en'], true) ? $lang : 'chs';
$tokenInUrl = str_replace('%3D', '=', urlencode($token));
$url = $baseUrl . '?token=' . $tokenInUrl . '&lang=' . $lang;
return [
'url' => $url,
'token' => $token,
'lang' => $lang,
'user_id' => (int) $player->id,
'user' => $userArr,
];
}
/**
* 从请求中解析 user-tokenheader: user-token 或 Authorization: Bearer
* @param object $request 需有 header(string $name) 方法
* 从 JWT 中解析 username仅解码 payload不校验签名与过期用于退出时清除会话
*/
public static function getTokenFromRequest(object $request): string
public static function getUsernameFromJwtPayload(string $token): ?string
{
$token = $request->header('user-token') ?? '';
if ($token !== '') {
return trim((string) $token);
}
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
return trim(substr($auth, 7));
}
return '';
}
/**
* 从请求获取当前用户 ID优先 request->user_id否则从 header 的 user-token 解析
* 中间件未正确注入时仍可兜底解析
* @param object $request 需有 user_id 属性及 header() 方法
*/
public static function getUserIdFromRequest(object $request): ?int
{
$id = $request->user_id ?? null;
if ($id !== null && (int) $id > 0) {
return (int) $id;
}
$token = self::getTokenFromRequest($request);
if ($token === '') {
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
return self::getUserIdFromToken($token);
}
/**
* 根据 user-token 获取 user_id不写缓存仅解析 JWT
* 若 token 已通过退出接口加入黑名单,返回 null
*/
public static function getUserIdFromToken(string $userToken): ?int
{
if (UserCache::isTokenBlacklisted($userToken)) {
$payload = base64_decode(strtr($parts[1], '-_', '+/'), true);
if ($payload === false) {
return null;
}
try {
$decoded = JwtToken::verify(1, $userToken);
$extend = $decoded['extend'] ?? [];
if (($extend['plat'] ?? '') !== 'api_user') {
return null;
}
$id = $extend['id'] ?? null;
if ($id === null) {
return null;
}
$userId = (int) $id;
// 同一用户只允许当前登记的 token 生效,重新登录/注册后旧 token 失效
if (!UserCache::isCurrentUserToken($userId, $userToken)) {
return null;
}
return $userId;
} catch (\Throwable $e) {
$data = json_decode($payload, true);
if (!is_array($data)) {
return null;
}
}
/**
* 退出登录:将当前 user-token 加入黑名单,使该 token 失效
*/
public static function logout(string $userToken): bool
{
try {
$decoded = JwtToken::verify(1, $userToken);
$exp = (int) ($decoded['exp'] ?? 0);
$ttl = $exp > time() ? $exp - time() : 86400;
return UserCache::addTokenToBlacklist($userToken, $ttl);
} catch (\Throwable $e) {
return false;
}
$extend = $data['extend'] ?? $data;
$username = $extend['username'] ?? null;
return $username !== null ? trim((string) $username) : null;
}
/**