diff --git a/server/.env.example b/server/.env.example index d404657..d4769e8 100644 --- a/server/.env.example +++ b/server/.env.example @@ -16,6 +16,9 @@ REDIS_PORT = 6379 REDIS_PASSWORD = '' REDIS_DB = 0 +# 游戏地址,用于 /api/v1/getGameUrl 返回 +GAME_URL = dice-game.yuliao666.top + # API 鉴权与用户(可选,不填则用默认值) # authToken 签名密钥(必填,与客户端约定,用于 signature 校验) API_AUTH_TOKEN_SECRET = xF75oK91TQj13s0UmNIr1NBWMWGfflNO diff --git a/server/app/api/cache/AuthTokenCache.php b/server/app/api/cache/AuthTokenCache.php new file mode 100644 index 0000000..16498e2 --- /dev/null +++ b/server/app/api/cache/AuthTokenCache.php @@ -0,0 +1,83 @@ +get('agent_id', ''))); + $secret = trim((string) ($request->get('secret', ''))); + $time = trim((string) ($request->get('time', ''))); + $signature = trim((string) ($request->get('signature', ''))); + + if ($agentId === '' || $secret === '' || $time === '' || $signature === '') { + return $this->fail('缺少参数:agent_id、secret、time、signature 不能为空', ReturnCode::PARAMS_ERROR); + } + + $expectedSecret = config('api.auth_token_secret', ''); + if ($expectedSecret === '') { + return $this->fail('服务端未配置 API_AUTH_TOKEN_SECRET', ReturnCode::SERVER_ERROR); + } + if ($secret !== $expectedSecret) { + return $this->fail('密钥错误', ReturnCode::FORBIDDEN); + } + + $timeVal = (int) $time; + $tolerance = (int) config('api.auth_token_time_tolerance', 300); + $now = time(); + if ($timeVal < $now - $tolerance || $timeVal > $now + $tolerance) { + return $this->fail('时间戳已过期或无效,请同步时间', ReturnCode::FORBIDDEN); + } + + $expectedSignature = md5($agentId . $secret . $time); + if ($signature !== $expectedSignature) { + return $this->fail('签名验证失败', ReturnCode::FORBIDDEN); + } + + $exp = (int) config('api.auth_token_exp', 86400); + $tokenResult = JwtToken::generateToken([ + 'id' => 0, + 'agent_id' => $agentId, + 'plat' => 'api_auth_token', + 'access_exp' => $exp, + ]); + $token = $tokenResult['access_token']; + if (!AuthTokenCache::setToken($agentId, $token)) { + return $this->fail('生成 token 失败', ReturnCode::SERVER_ERROR); + } + + return $this->success([ + 'authtoken' => $token, + ]); + } +} diff --git a/server/app/api/controller/v1/GameController.php b/server/app/api/controller/v1/GameController.php new file mode 100644 index 0000000..4dea09c --- /dev/null +++ b/server/app/api/controller/v1/GameController.php @@ -0,0 +1,55 @@ +post('username', ''))); + $password = trim((string) ($request->post('password', '123456'))); + $time = trim((string) ($request->post('time', ''))); + + if ($username === '') { + return $this->fail('username 不能为空', ReturnCode::PARAMS_ERROR); + } + if ($password === '') { + $password = '123456'; + } + if ($time === '') { + $time = (string) time(); + } + + try { + $logic = new UserLogic(); + $result = $logic->loginByUsername($username, $password, 'chs', 0.0, $time); + } catch (\plugin\saiadmin\exception\ApiException $e) { + return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR); + } + + $gameUrlBase = rtrim(config('api.game_url', 'dice-game.yuliao666.top'), '/'); + $tokenInUrl = str_replace('%3D', '=', urlencode($result['token'])); + $url = $gameUrlBase . '/?token=' . $tokenInUrl; + + return $this->success([ + 'url' => $url, + ]); + } +} diff --git a/server/app/api/middleware/AuthTokenMiddleware.php b/server/app/api/middleware/AuthTokenMiddleware.php new file mode 100644 index 0000000..5357b06 --- /dev/null +++ b/server/app/api/middleware/AuthTokenMiddleware.php @@ -0,0 +1,58 @@ +agent_id + */ +class AuthTokenMiddleware implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + $token = $request->header('auth-token'); + $token = $token !== null ? trim((string) $token) : ''; + if ($token === '') { + throw new ApiException('请携带 auth-token', ReturnCode::UNAUTHORIZED); + } + + try { + $decoded = JwtToken::verify(1, $token); + } catch (JwtTokenExpiredException $e) { + throw new ApiException('auth-token 已过期', ReturnCode::TOKEN_INVALID); + } catch (JwtTokenException $e) { + throw new ApiException('auth-token 无效', ReturnCode::TOKEN_INVALID); + } catch (\Throwable $e) { + throw new ApiException('auth-token 格式无效', ReturnCode::TOKEN_INVALID); + } + + $extend = $decoded['extend'] ?? []; + if ((string) ($extend['plat'] ?? '') !== 'api_auth_token') { + throw new ApiException('auth-token 无效', ReturnCode::TOKEN_INVALID); + } + $agentId = trim((string) ($extend['agent_id'] ?? '')); + if ($agentId === '') { + throw new ApiException('auth-token 无效', ReturnCode::TOKEN_INVALID); + } + + $currentToken = AuthTokenCache::getTokenByAgentId($agentId); + if ($currentToken === null || $currentToken !== $token) { + throw new ApiException('auth-token 无效或已失效', ReturnCode::TOKEN_INVALID); + } + + $request->agent_id = $agentId; + return $handler($request); + } +} diff --git a/server/config/api.php b/server/config/api.php index 2a0909d..4e7148a 100644 --- a/server/config/api.php +++ b/server/config/api.php @@ -5,6 +5,8 @@ return [ // 登录成功返回的连接地址前缀,如 https://127.0.0.1:6777 'login_url_base' => env('API_LOGIN_URL_BASE', 'https://127.0.0.1:6777'), + // 游戏地址,用于 /api/v1/getGameUrl 返回拼接 token + 'game_url' => env('GAME_URL', 'dice-game.yuliao666.top'), // 按 username 存储的登录会话 Redis key 前缀,用于 token 中间件校验 'session_username_prefix' => env('API_SESSION_USERNAME_PREFIX', 'api:user:session:'), // 登录会话过期时间(秒),默认 7 天 @@ -17,6 +19,8 @@ return [ 'auth_token_exp' => (int) env('API_AUTH_TOKEN_EXP', 86400), // auth-token 按设备存储的 Redis key 前缀(同一设备只保留最新一个 auth-token) 'auth_token_device_prefix' => env('API_AUTH_TOKEN_DEVICE_PREFIX', 'api:auth_token:'), + // auth-token 按 token 存储的 Redis key 前缀(用于校验 auth-token 请求头) + 'auth_token_prefix' => env('API_AUTH_TOKEN_PREFIX', 'api:auth_token:t:'), // user-token 有效期(秒),默认 7 天 'user_token_exp' => (int) env('API_USER_TOKEN_EXP', 604800), // 按用户存储当前有效 user-token 的 Redis key 前缀(同一用户仅保留最新一次登录的 token) diff --git a/server/config/route.php b/server/config/route.php index af2598c..8854006 100644 --- a/server/config/route.php +++ b/server/config/route.php @@ -14,6 +14,19 @@ use Webman\Route; use app\api\middleware\TokenMiddleware; +use app\api\middleware\AuthTokenMiddleware; + +// 平台鉴权接口:/api/v1/authToken,请求头 signature/secret/time/agent_id,返回 authtToken +Route::group('/api/v1', function () { + Route::any('/authToken', [app\api\controller\v1\AuthTokenController::class, 'index']); +})->middleware([]); + +// 平台 v1 接口:需在请求头携带 auth-token +Route::group('/api/v1', function () { + Route::any('/getGameUrl', [app\api\controller\v1\GameController::class, 'getGameUrl']); +})->middleware([ + AuthTokenMiddleware::class, +]); // 登录接口:无需 token,提交 JSON 获取带 token 的连接地址 Route::group('/api', function () {