where('token', $token)->where('expire_time', '>', time())->whereIn('terminal', self::SESSION_TERMINALS)->find() : null; $admin = $session ? Admin::where('id', '=', (int) $session['admin_id'])->findOrEmpty() : null; if ($admin === null || $admin->isEmpty()) { throw new McpException('后台登录已失效,请输入账号密码', 'session_invalid', 401); } GrantService::assertAdminUsable($admin); if (McpConfig::requirePasswordChanged() && array_key_exists('is_paw', $admin->getData()) && (int) $admin['is_paw'] !== 1) { throw new McpException('请先在甄养堂后台修改初始密码,再登录行知', 'need_change_password'); } return $admin; } /** 一次性授权码:只存哈希,绑定账号、客户端和回调地址 */ public static function issueCode(Admin $admin, string $clientId, string $redirectUri, string $via): string { $code = bin2hex(random_bytes(32)); Cache::set(self::codeKey($code), [ 'admin_id' => (int) $admin['id'], 'client_id' => $clientId, 'redirect_uri' => $redirectUri, 'via' => $via, 'issued' => time(), ], self::CODE_TTL); return $code; } /** * 兑换授权码(只能用一次),返回 [后台账号, 确认方式];账号此刻仍须满足各项门禁。 */ public static function redeem(string $code, string $clientId, string $redirectUri): array { $data = preg_match('/^[0-9a-f]{64}$/', $code) === 1 ? self::take(self::codeKey($code)) : null; if (!is_array($data) || !hash_equals((string) $data['client_id'], $clientId) || $data['redirect_uri'] !== $redirectUri || time() - (int) $data['issued'] > self::CODE_TTL) { throw new McpException('登录授权码无效、已过期或已使用,请重新登录', 'invalid_grant', 400); } $admin = Admin::where('id', '=', (int) $data['admin_id'])->findOrEmpty(); if ($admin->isEmpty()) { throw new McpException('甄养堂账号已删除', 'invalid_grant', 400); } GrantService::assertAdminUsable($admin); return [$admin, (string) $data['via']]; } /** 回到行知的地址:登记的 redirect_uri 加上参数 */ public static function redirectWith(string $redirectUri, array $params): string { return $redirectUri . (str_contains($redirectUri, '?') ? '&' : '?') . http_build_query($params); } private static function codeKey(string $code): string { return 'ai_sso_code_' . hash('sha256', $code); } /** 读取并删除;加文件锁,同一授权码并发兑换时只有一个能拿到 */ private static function take(string $key) { $lock = fopen(runtime_path() . 'ai_sso.lock', 'c'); if ($lock === false) { return null; } try { flock($lock, LOCK_EX); $data = Cache::get($key); if ($data !== null) { Cache::delete($key); } return $data; } finally { flock($lock, LOCK_UN); fclose($lock); } } }