diff --git a/src/Base/SessionMiddleware.php b/src/Base/SessionMiddleware.php new file mode 100644 index 0000000..d93a0f6 --- /dev/null +++ b/src/Base/SessionMiddleware.php @@ -0,0 +1,45 @@ +handle($request); + + // 保存 Session + Session::save(); + + return $response; + } catch (\Throwable $e) { + // 即使出错也保存 Session + Session::save(); + throw $e; + } + } +} + diff --git a/src/Session.php b/src/Session.php new file mode 100644 index 0000000..0b26c29 --- /dev/null +++ b/src/Session.php @@ -0,0 +1,416 @@ +getCookieParams(); + + // 从 Cookie 中获取 Session ID + if (isset($cookies[self::SESSION_NAME]) && !empty($cookies[self::SESSION_NAME])) { + $sessionId = $cookies[self::SESSION_NAME]; + // 验证 Session ID 格式 + if (preg_match('/^[a-zA-Z0-9]{32,}$/', $sessionId)) { + return $sessionId; + } + } + + // 生成新的 Session ID + return self::generateSessionId(); + } + + + /** + * 生成 Session ID + * @return string + */ + private static function generateSessionId(): string + { + return bin2hex(random_bytes(16)); + } + + + /** + * 加载 Session 数据 + * @param string $sessionId + * @return array + */ + private static function loadSessionData(string $sessionId): array + { + $filePath = self::getSessionFilePath($sessionId); + + if (!file_exists($filePath)) { + return []; + } + + $data = file_get_contents($filePath); + if ($data === false) { + return []; + } + + $sessionData = json_decode($data, true); + if (!is_array($sessionData)) { + return []; + } + + // 检查是否过期 + if (isset($sessionData['expires_at']) && $sessionData['expires_at'] < time()) { + self::destroySession($sessionId); + return []; + } + + return $sessionData['data'] ?? []; + } + + + /** + * 保存 Session 数据 + * @return void + * @throws + */ + public static function save(): void + { + if (!self::$started) { + return; + } + + $sessionId = Context::get(self::SESSION_ID_KEY); + $sessionData = Context::get(self::SESSION_KEY); + + if ($sessionId === null || $sessionData === null) { + return; + } + + $filePath = self::getSessionFilePath($sessionId); + $data = [ + 'data' => $sessionData, + 'expires_at' => time() + self::$lifetime, + 'created_at' => time(), + ]; + + file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE)); + + // 设置 Cookie + self::setSessionCookie($sessionId); + } + + + /** + * 设置 Session Cookie + * @param string $sessionId + * @return void + * @throws + */ + private static function setSessionCookie(string $sessionId): void + { + $response = \response(); + $expires = time() + self::$lifetime; + + // 获取现有的 Cookie 参数 + $cookieParams = $response->getCookieParams(); + + // 添加 Session Cookie + // Swoole setCookie 参数: name, value, expires, path, domain, secure, httponly, samesite + $cookieParams[] = [ + self::SESSION_NAME, // name + $sessionId, // value + $expires, // expires + '/', // path + '', // domain + false, // secure + true, // httponly + 'Lax' // samesite + ]; + + $response->withCookieParams($cookieParams); + } + + + /** + * 获取 Session 文件路径 + * @param string $sessionId + * @return string + */ + private static function getSessionFilePath(string $sessionId): string + { + if (self::$savePath === null) { + self::init(); + } + + return self::$savePath . '/' . $sessionId . '.json'; + } + + + /** + * 销毁 Session 文件 + * @param string $sessionId + * @return void + */ + private static function destroySession(string $sessionId): void + { + $filePath = self::getSessionFilePath($sessionId); + if (file_exists($filePath)) { + unlink($filePath); + } + } + + + /** + * 存储 Session 数据 + * @param string|array $key 键名或键值对数组 + * @param mixed $value 值(当 $key 为字符串时) + * @return void + * @throws + */ + public static function put(string|array $key, mixed $value = null): void + { + if (!self::$started) { + self::start(); + } + + $sessionData = Context::get(self::SESSION_KEY, []); + + // 支持批量设置 + if (is_array($key)) { + $sessionData = array_merge($sessionData, $key); + } else { + $sessionData[$key] = $value; + } + + Context::set(self::SESSION_KEY, $sessionData); + } + + + /** + * 获取 Session 数据 + * @param string|null $key 键名,为 null 时返回所有数据 + * @param mixed $default 默认值 + * @return mixed + * @throws + */ + public static function get(?string $key = null, mixed $default = null): mixed + { + if (!self::$started) { + self::start(); + } + + $sessionData = Context::get(self::SESSION_KEY, []); + + // 如果 key 为 null,返回所有数据 + if ($key === null) { + return $sessionData; + } + + return $sessionData[$key] ?? $default; + } + + + /** + * 检查 Session 中是否存在指定键 + * @param string $key + * @return bool + * @throws + */ + public static function has(string $key): bool + { + if (!self::$started) { + self::start(); + } + + $sessionData = Context::get(self::SESSION_KEY, []); + return isset($sessionData[$key]); + } + + + /** + * 删除 Session 数据 + * @param string $key + * @return void + * @throws + */ + public static function forget(string $key): void + { + if (!self::$started) { + self::start(); + } + + $sessionData = Context::get(self::SESSION_KEY, []); + unset($sessionData[$key]); + Context::set(self::SESSION_KEY, $sessionData); + } + + + /** + * 清空所有 Session 数据 + * @return void + * @throws + */ + public static function flush(): void + { + if (!self::$started) { + self::start(); + } + + Context::set(self::SESSION_KEY, []); + } + + + /** + * 获取所有 Session 数据 + * @return array + * @throws + */ + public static function all(): array + { + if (!self::$started) { + self::start(); + } + + return Context::get(self::SESSION_KEY, []); + } + + + /** + * 销毁 Session + * @return void + * @throws + */ + public static function destroy(): void + { + if (!self::$started) { + return; + } + + $sessionId = Context::get(self::SESSION_ID_KEY); + if ($sessionId !== null) { + self::destroySession($sessionId); + } + + Context::set(self::SESSION_KEY, []); + Context::set(self::SESSION_ID_KEY, null); + self::$started = false; + } + + + /** + * 重新生成 Session ID + * @return void + * @throws + */ + public static function regenerate(): void + { + if (!self::$started) { + self::start(); + } + + $oldSessionId = Context::get(self::SESSION_ID_KEY); + $sessionData = Context::get(self::SESSION_KEY, []); + + // 生成新的 Session ID + $newSessionId = self::generateSessionId(); + + // 删除旧 Session 文件 + if ($oldSessionId !== null) { + self::destroySession($oldSessionId); + } + + // 保存新 Session + Context::set(self::SESSION_ID_KEY, $newSessionId); + self::setSessionCookie($newSessionId); + } +} +