diff --git a/server/app/api/cache/AuthTokenCache.php b/server/app/api/cache/AuthTokenCache.php deleted file mode 100644 index 7e1d28e..0000000 --- a/server/app/api/cache/AuthTokenCache.php +++ /dev/null @@ -1,54 +0,0 @@ -method()) !== 'GET') { - return $this->fail('仅支持 GET 请求', ReturnCode::PARAMS_ERROR); - } - - $param = $request->get(); - $signature = trim((string) ($param['signature'] ?? '')); - $secret = trim((string) ($param['secret'] ?? '')); - $device = trim((string) ($param['device'] ?? '')); - $time = trim((string) ($param['time'] ?? '')); - - if ($signature === '' || $secret === '' || $device === '' || $time === '') { - return $this->fail('signature、secret、device、time 均为必传且不能为空', ReturnCode::PARAMS_ERROR); - } - - $serverSecret = trim((string) config('api.auth_token_secret', '')); - if ($serverSecret === '') { - return $this->fail('服务未配置 API_AUTH_TOKEN_SECRET', ReturnCode::PARAMS_ERROR); - } - if ($secret !== $serverSecret) { - return $this->fail('密钥错误', ReturnCode::FORBIDDEN); - } - - $tolerance = (int) config('api.auth_token_time_tolerance', 300); - $now = time(); - $ts = is_numeric($time) ? (int) $time : 0; - if ($ts <= 0 || abs($now - $ts) > $tolerance) { - return $this->fail('时间戳无效或已过期', ReturnCode::PARAMS_ERROR); - } - - $sign = $this->getAuthToken($device, $serverSecret, $time); - if ($sign !== $signature) { - return $this->fail('签名验证失败', ReturnCode::FORBIDDEN); - } - - $exp = (int) config('api.auth_token_exp', 86400); - $tokenResult = JwtToken::generateToken([ - 'id' => 0, - 'plat' => 'api', - 'device' => $device, - 'access_exp' => $exp, - ]); - - // 同一设备只保留最新 token,覆盖后旧 token 失效 - AuthTokenCache::setDeviceToken($device, $tokenResult['access_token'], $exp); - - return $this->success([ - 'auth-token' => $tokenResult['access_token'], - 'expires_in' => $tokenResult['expires_in'], - ]); - } - - /** - * 生成签名:signature = md5(device . secret . time) - * - * @param string $device 设备标识 - * @param string $secret 密钥(来自配置) - * @param string $time 时间戳 - * @return string - */ - private function getAuthToken(string $device, string $secret, string $time): string - { - return md5($device . $secret . $time); - } -} diff --git a/server/app/api/controller/GameController.php b/server/app/api/controller/GameController.php index e71ba8d..d0b9120 100644 --- a/server/app/api/controller/GameController.php +++ b/server/app/api/controller/GameController.php @@ -7,7 +7,6 @@ use support\Request; use support\Response; use app\api\logic\GameLogic; use app\api\logic\PlayStartLogic; -use app\api\logic\UserLogic; use app\api\util\ReturnCode; use app\dice\model\play_record\DicePlayRecord; use app\dice\model\player\DicePlayer; @@ -23,12 +22,12 @@ class GameController extends OpenController /** * 购买抽奖券 * POST /api/game/buyLotteryTickets - * header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id) + * header: token(由 TokenMiddleware 注入 request->player_id) * body: count = 1 | 5 | 10(1次/100coin, 5次/500coin, 10次/1000coin) */ public function buyLotteryTickets(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $count = (int) $request->post('count', 0); if (!in_array($count, [1, 5, 10], true)) { return $this->fail('购买抽奖券错误', ReturnCode::PARAMS_ERROR); @@ -52,7 +51,7 @@ class GameController extends OpenController /** * 获取彩金池(中奖配置表) * GET /api/game/lotteryPool - * header: auth-token + * header: token * 返回 DiceRewardConfig 列表(彩金池/中奖配置) */ public function lotteryPool(Request $request): Response @@ -64,12 +63,12 @@ class GameController extends OpenController /** * 开始游戏(抽奖一局) * POST /api/game/playStart - * header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id) + * header: token(由 TokenMiddleware 注入 request->player_id) * body: rediction 必传,0=无 1=中奖 */ public function playStart(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $rediction = $request->post('rediction'); if ($rediction === '' || $rediction === null) { return $this->fail('请传递 rediction 参数', ReturnCode::PARAMS_ERROR); diff --git a/server/app/api/controller/UserController.php b/server/app/api/controller/UserController.php index 0a416bd..ddfc8ca 100644 --- a/server/app/api/controller/UserController.php +++ b/server/app/api/controller/UserController.php @@ -13,77 +13,85 @@ use app\dice\model\player_wallet_record\DicePlayerWalletRecord; use plugin\saiadmin\basic\OpenController; /** - * API 用户登录/注册 - * 需先携带 auth-token,登录/注册成功后返回 user-token 与用户信息,用户信息已写入 Redis(key=base64(user_id),value=加密) + * API 用户登录等 + * 登录接口 /api/user/Login 无需 token;其余接口需在请求头携带 token(base64(username.-.time)),由 TokenMiddleware 鉴权并注入 request->player_id / request->player */ class UserController extends OpenController { /** - * 登录 - * POST /api/user/login - * body: phone (+60), password + * 登录(JSON body) + * POST /api/user/Login + * body: { "username": "+60123456789", "password": "123456", "lang": "chs", "coin": 2000.00, "time": 1772692089 } + * 根据 username 查找或创建 DicePlayer,按 coin 增减平台币,会话写 Redis,返回带 token 的连接地址 */ - public function login(Request $request): Response + public function Login(Request $request): Response { - $phone = $request->post('phone', ''); - $password = $request->post('password', ''); - if ($phone === '' || $password === '') { - return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR); + $body = $request->rawBody(); + if ($body === '' || $body === null) { + return $this->fail('请提交 JSON body', ReturnCode::PARAMS_ERROR); + } + $data = json_decode($body, true); + if (!is_array($data)) { + return $this->fail('JSON 格式错误', ReturnCode::PARAMS_ERROR); + } + $username = trim((string) ($data['username'] ?? '')); + $password = trim((string) ($data['password'] ?? '')); + $lang = trim((string) ($data['lang'] ?? 'chs')); + $coin = isset($data['coin']) ? (float) $data['coin'] : 0.0; + $time = isset($data['time']) ? (string) $data['time'] : (string) time(); + if ($username === '' || $password === '') { + return $this->fail('username、password 不能为空', ReturnCode::PARAMS_ERROR); } - $logic = new UserLogic(); - $data = $logic->login($phone, $password); - return $this->success([ - 'user' => $data['user'], - 'user-token' => $data['user-token'], - 'user_id' => $data['user_id'], - ]); - } - /** - * 注册 - * POST /api/user/register - * body: phone (+60), password, nickname(可选) - */ - public function register(Request $request): Response - { - $phone = $request->post('phone', ''); - $password = $request->post('password', ''); - $nickname = $request->post('nickname'); - if ($phone === '' || $password === '') { - return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR); + try { + $logic = new UserLogic(); + $result = $logic->loginByUsername($username, $password, $lang, $coin, $time); + return $this->success([ + 'url' => $result['url'], + 'token' => $result['token'], + 'lang' => $result['lang'], + 'user_id' => $result['user_id'], + 'user' => $result['user'], + ]); + } catch (\plugin\saiadmin\exception\ApiException $e) { + return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR); } - $logic = new UserLogic(); - $data = $logic->register($phone, $password, $nickname ? (string) $nickname : null); - return $this->success([ - 'user' => $data['user'], - 'user-token' => $data['user-token'], - 'user_id' => $data['user_id'], - ]); } /** * 退出登录 * POST /api/user/logout - * header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->userToken) + * header: token(JWT),清除该 username 的 Redis 会话 */ public function logout(Request $request): Response { - $token = $request->userToken ?? UserLogic::getTokenFromRequest($request); - if ($token === '' || !UserLogic::logout($token)) { - return $this->fail('退出失败或 token 已失效', ReturnCode::TOKEN_INVALID); + $token = $request->header('token'); + if ($token === null || $token === '') { + $auth = $request->header('authorization'); + if ($auth && stripos($auth, 'Bearer ') === 0) { + $token = trim(substr($auth, 7)); + } } + $token = $token !== null ? trim((string) $token) : ''; + if ($token === '') { + return $this->fail('请携带 token', ReturnCode::UNAUTHORIZED); + } + $username = UserLogic::getUsernameFromJwtPayload($token); + if ($username === null || $username === '') { + return $this->fail('token 无效', ReturnCode::TOKEN_INVALID); + } + UserCache::deleteSessionByUsername($username); return $this->success('已退出登录'); } /** * 获取当前用户信息 * GET /api/user/info - * header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id) - * 返回:id, username, phone, uid, name, coin, total_ticket_count + * header: token(由 TokenMiddleware 校验并注入 request->player_id) */ public function info(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $user = UserLogic::getCachedUser($userId); if (empty($user)) { return $this->fail('用户不存在', ReturnCode::NOT_FOUND); @@ -99,13 +107,13 @@ class UserController extends OpenController } /** - * 获取钱包余额(优先读缓存,缓存未命中时从库拉取并回写缓存) + * 获取钱包余额(优先读缓存) * GET /api/user/balance - * header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id) + * header: token(由 TokenMiddleware 注入 request->player_id) */ public function balance(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $user = UserLogic::getCachedUser($userId); if (empty($user)) { return $this->fail('用户不存在', ReturnCode::NOT_FOUND); @@ -124,12 +132,12 @@ class UserController extends OpenController /** * 玩家钱包流水 * GET /api/user/walletRecord - * header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id) + * header: token(由 TokenMiddleware 注入 request->player_id) * 参数: page 页码(默认1), limit 每页条数(默认10), create_time_min/create_time_max 创建时间范围(可选) */ public function walletRecord(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $page = (int) $request->post('page', 1); $limit = (int) $request->post('limit', 10); if ($page < 1) { @@ -166,12 +174,12 @@ class UserController extends OpenController /** * 游玩记录 * GET /api/user/playGameRecord - * header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id) + * header: token(由 TokenMiddleware 注入 request->player_id) * 参数: page 页码(默认1), limit 每页条数(默认10), create_time_min/create_time_max 创建时间范围(可选) */ public function playGameRecord(Request $request): Response { - $userId = UserLogic::getUserIdFromRequest($request) ?? 0; + $userId = (int) ($request->player_id ?? 0); $page = (int) $request->post('page', 1); $limit = (int) $request->post('limit', 10); if ($page < 1) { diff --git a/server/app/api/logic/UserLogic.php b/server/app/api/logic/UserLogic.php index 3246c8f..6d23e46 100644 --- a/server/app/api/logic/UserLogic.php +++ b/server/app/api/logic/UserLogic.php @@ -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-token(JWT,plat=api_user,id=user_id) + * 登录(JSON:username, 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-token(header: 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; } /** diff --git a/server/app/api/middleware/CheckAuthTokenMiddleware.php b/server/app/api/middleware/CheckAuthTokenMiddleware.php deleted file mode 100644 index 3c33ec1..0000000 --- a/server/app/api/middleware/CheckAuthTokenMiddleware.php +++ /dev/null @@ -1,109 +0,0 @@ -,且必须通过 JWT 签名与 plat=api 校验 - */ -class CheckAuthTokenMiddleware implements MiddlewareInterface -{ - /** 不需要 auth-token 的路径 */ - private const WHITELIST = [ - 'api/authToken', - ]; - - /** JWT 至少为 xxx.yyy.zzz 三段 */ - private const JWT_PARTS_MIN = 3; - - public function process(Request $request, callable $handler): Response - { - $path = trim((string) $request->path(), '/'); - if ($this->isWhitelist($path)) { - return $handler($request); - } - - $token = $this->getAuthTokenFromRequest($request); - if ($token === '') { - throw new ApiException('请携带 auth-token', ReturnCode::UNAUTHORIZED); - } - - if (!$this->looksLikeJwt($token)) { - throw new ApiException('auth-token 格式无效', ReturnCode::TOKEN_INVALID); - } - - $decoded = $this->verifyAuthToken($token); - $extend = $decoded['extend'] ?? []; - if (($extend['plat'] ?? '') !== 'api') { - throw new ApiException('auth-token 无效(非 API 凭证)', ReturnCode::TOKEN_INVALID); - } - - // 同一设备只允许一个 auth-token 生效,非当前 token 视为已失效 - $device = (string) ($extend['device'] ?? ''); - if ($device !== '' && !AuthTokenCache::isCurrentToken($device, $token)) { - throw new ApiException('auth-token 已失效(该设备已签发新凭证,请使用新 auth-token)', ReturnCode::TOKEN_INVALID); - } - - return $handler($request); - } - - private function getAuthTokenFromRequest(Request $request): string - { - $token = $request->header('auth-token'); - if ($token !== null && $token !== '') { - return trim((string) $token); - } - $auth = $request->header('authorization'); - if ($auth && stripos($auth, 'Bearer ') === 0) { - return trim(substr($auth, 7)); - } - return ''; - } - - private function looksLikeJwt(string $token): bool - { - $parts = explode('.', $token); - return count($parts) >= self::JWT_PARTS_MIN; - } - - /** - * 校验 auth-token 有效性(签名、过期、iss 等),无效或过期必抛 ApiException - */ - private function verifyAuthToken(string $token): array - { - try { - return JwtToken::verify(1, $token); - } catch (JwtTokenExpiredException $e) { - Log::error('auth-token 已过期, 报错信息' . $e); - throw new ApiException('auth-token 已过期', ReturnCode::TOKEN_INVALID); - } catch (JwtTokenException $e) { - Log::error('auth-token 无效, 报错信息' . $e); - throw new ApiException($e->getMessage() ?: 'auth-token 无效', ReturnCode::TOKEN_INVALID); - } catch (\Throwable $e) { - Log::error('auth-token 校验失败, 报错信息' . $e); - throw new ApiException('auth-token 校验失败', ReturnCode::TOKEN_INVALID); - } - } - - private function isWhitelist(string $path): bool - { - foreach (self::WHITELIST as $prefix) { - if ($path === $prefix || str_starts_with($path, $prefix . '/')) { - return true; - } - } - return false; - } -} diff --git a/server/app/api/middleware/CheckUserTokenMiddleware.php b/server/app/api/middleware/CheckUserTokenMiddleware.php deleted file mode 100644 index f7f4442..0000000 --- a/server/app/api/middleware/CheckUserTokenMiddleware.php +++ /dev/null @@ -1,42 +0,0 @@ -,校验通过后将 user_id、userToken 写入 request 供控制器使用 - */ -class CheckUserTokenMiddleware implements MiddlewareInterface -{ - public function process(Request $request, callable $handler): Response - { - $token = $request->header('user-token'); - if (empty($token)) { - $auth = $request->header('authorization'); - if ($auth && stripos($auth, 'Bearer ') === 0) { - $token = trim(substr($auth, 7)); - } - } - if (empty($token)) { - throw new ApiException('请携带 user-token', ReturnCode::UNAUTHORIZED); - } - - $userId = UserLogic::getUserIdFromToken($token); - if ($userId === null) { - throw new ApiException('user-token 无效或已过期', ReturnCode::TOKEN_INVALID); - } - - $request->user_id = $userId; - $request->userToken = $token; - - return $handler($request); - } -} diff --git a/server/app/api/middleware/TokenMiddleware.php b/server/app/api/middleware/TokenMiddleware.php new file mode 100644 index 0000000..37f18db --- /dev/null +++ b/server/app/api/middleware/TokenMiddleware.php @@ -0,0 +1,78 @@ +player_id、request->player + */ +class TokenMiddleware implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + $token = $request->header('token'); + if ($token === null || $token === '') { + $auth = $request->header('authorization'); + if ($auth && stripos($auth, 'Bearer ') === 0) { + $token = trim(substr($auth, 7)); + } + } + $token = $token !== null ? trim((string) $token) : ''; + if ($token === '') { + throw new ApiException('请携带 token', ReturnCode::UNAUTHORIZED); + } + + try { + $decoded = JwtToken::verify(1, $token); + } catch (JwtTokenExpiredException $e) { + throw new ApiException('token 已过期,请重新登录', ReturnCode::TOKEN_INVALID); + } catch (JwtTokenException $e) { + throw new ApiException('token 无效', ReturnCode::TOKEN_INVALID); + } catch (\Throwable $e) { + throw new ApiException('token 格式无效', ReturnCode::TOKEN_INVALID); + } + + $extend = $decoded['extend'] ?? []; + if ((string) ($extend['plat'] ?? '') !== 'api_login') { + throw new ApiException('token 无效', ReturnCode::TOKEN_INVALID); + } + $username = trim((string) ($extend['username'] ?? '')); + if ($username === '') { + throw new ApiException('token 无效', ReturnCode::TOKEN_INVALID); + } + + $currentToken = UserCache::getSessionTokenByUsername($username); + if ($currentToken === null || $currentToken === '') { + $player = DicePlayer::where('username', $username)->find(); + if (!$player) { + throw new ApiException('请注册', ReturnCode::TOKEN_INVALID); + } + throw new ApiException('请重新登录', ReturnCode::TOKEN_INVALID); + } + if ($currentToken !== $token) { + throw new ApiException('请重新登录(当前账号已在其他处登录)', ReturnCode::TOKEN_INVALID); + } + + $player = DicePlayer::where('username', $username)->find(); + if (!$player) { + UserCache::deleteSessionByUsername($username); + throw new ApiException('请重新登录', ReturnCode::TOKEN_INVALID); + } + $request->player_id = (int) $player->id; + $request->player = $player; + return $handler($request); + } +} diff --git a/server/config/api.php b/server/config/api.php index a5e86e0..3abb2e2 100644 --- a/server/config/api.php +++ b/server/config/api.php @@ -3,6 +3,12 @@ * API 鉴权与用户相关配置 */ return [ + // 登录成功返回的连接地址前缀,如 https://127.0.0.1:6777 + 'login_url_base' => env('API_LOGIN_URL_BASE', 'https://127.0.0.1:6777'), + // 按 username 存储的登录会话 Redis key 前缀,用于 token 中间件校验 + 'session_username_prefix' => env('API_SESSION_USERNAME_PREFIX', 'api:user:session:'), + // 登录会话过期时间(秒),默认 7 天 + 'session_expire' => (int) env('API_SESSION_EXPIRE', 604800), // auth-token 签名密钥(与客户端约定,用于 /api/authToken 的 signature 校验,必填) 'auth_token_secret' => env('API_AUTH_TOKEN_SECRET', ''), // auth-token 时间戳允许误差(秒),防重放,默认 300 秒 diff --git a/server/config/route.php b/server/config/route.php index 5881e43..f584f39 100644 --- a/server/config/route.php +++ b/server/config/route.php @@ -13,19 +13,14 @@ */ use Webman\Route; -use app\api\middleware\CheckAuthTokenMiddleware; -use app\api\middleware\CheckUserTokenMiddleware; +use app\api\middleware\TokenMiddleware; -// 仅需 auth-token 的路由组(authToken 接口在中间件内白名单跳过) +// 登录接口:无需 token,提交 JSON 获取带 token 的连接地址 Route::group('/api', function () { - Route::any('/authToken', [app\api\controller\AuthTokenController::class, 'index']); - Route::any('/user/login', [app\api\controller\UserController::class, 'login']); - Route::any('/user/register', [app\api\controller\UserController::class, 'register']); -})->middleware([ - CheckAuthTokenMiddleware::class, -]); + Route::any('/user/Login', [app\api\controller\UserController::class, 'Login']); +})->middleware([]); -// 需 auth-token + user-token 的路由组 +// 其余接口:仅经 token 中间件鉴权(header: token,base64(username.-.time)) Route::group('/api', function () { Route::any('/user/logout', [app\api\controller\UserController::class, 'logout']); Route::any('/user/info', [app\api\controller\UserController::class, 'info']); @@ -36,6 +31,5 @@ Route::group('/api', function () { Route::any('/game/lotteryPool', [app\api\controller\GameController::class, 'lotteryPool']); Route::any('/game/playStart', [app\api\controller\GameController::class, 'playStart']); })->middleware([ - CheckAuthTokenMiddleware::class, - CheckUserTokenMiddleware::class, + TokenMiddleware::class, ]); diff --git a/server/support/Request.php b/server/support/Request.php index b7f0a31..66240b0 100644 --- a/server/support/Request.php +++ b/server/support/Request.php @@ -20,11 +20,11 @@ namespace support; */ class Request extends \Webman\Http\Request { - /** 由 CheckUserTokenMiddleware 注入:当前用户 ID */ - public ?int $user_id = null; + /** 由 TokenMiddleware 注入:当前玩家 ID(DicePlayer.id) */ + public ?int $player_id = null; - /** 由 CheckUserTokenMiddleware 注入:当前 user-token 原始字符串 */ - public ?string $userToken = null; + /** 由 TokenMiddleware 注入:当前玩家模型实例 */ + public $player = null; /** * 获取参数增强方法