PHP自定义JSON会话处理器误销毁会话问题排查
PHP自定义会话处理器会话销毁问题排查与解决
问题描述
需要实现PHP自定义会话处理器,将会话数据存储为指定目录下的JSON文件,核心代码如下:
class CustomSessionHandler implements SessionHandlerInterface { private string $savePath; public function close(): bool { return true; } public function destroy(string $id): bool { $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . "sess_" . $id . ".json"; if (file_exists($sessionPath)) { unlink($sessionPath); } return true; } public function gc(int $max_lifetime): int|false { foreach (glob($this->savePath . DIRECTORY_SEPARATOR . "sess_*" . ".json") as $file) { if (filemtime($file) + $max_lifetime < time() && file_exists($file)) { unlink($file); } } return true; } public function open(string $path, string $name): bool { $this->savePath = $path; if (!is_dir($path)) { mkdir($path); } return true; } public function read(string $id): string { $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . "sess_" . $id . ".json"; if (!file_exists($sessionPath)) { file_put_contents($sessionPath, ""); chmod($sessionPath, 0777); } return file_get_contents($sessionPath); } public function write(string $id, string $data): bool { $rawData = []; try { $rawData = self::unserialize_php($data); } catch (Exception $e) { echo $e->getMessage(); } $jsonData = json_encode($rawData); return !(file_put_contents($this->savePath . DIRECTORY_SEPARATOR . 'sess_' . $id . ".json", $jsonData) === false); } /** * @throws Exception */ private static function unserialize_php($sessionData): array { $returnData = []; $offset = 0; while ($offset < strlen($sessionData)) { if (!str_contains(substr($sessionData, $offset), "|")) { throw new Exception("invalid data, remaining: " . substr($sessionData, $offset)); } $pos = strpos($sessionData, "|", $offset); $num = $pos - $offset; $varName = substr($sessionData, $offset, $num); $offset += $num + 1; $data = unserialize(substr($sessionData, $offset)); $returnData[$varName] = $data; $offset += strlen(serialize($data)); } return $returnData; } }
在login.php和welcome.php中通过以下方式启动会话:
$handler = new CustomSessionHandler(); session_set_save_handler($handler); session_start([ 'save_path' => '/var/www/phpcourse.loc/hometask_3/sessions', ]);
出现异常:会话文件被错误创建和销毁,无法登录,触发警告session_start(): Failed to decode session object. Session has been destroyed。调试发现:
- 首次加载
login.php时,会话文件被创建为空; - 点击登录后,
session_start执行时,read方法返回后会话文件立即被销毁; - 验证通过后重新创建会话文件并写入JSON数据,但进入
welcome.php执行session_start时,会话文件再次被销毁,跳回登录页。
核心原因
- 会话格式不匹配:PHP会话系统要求
read()方法返回PHP原生序列化格式的字符串(即key|serialized_value格式),但当前read()直接返回JSON字符串,PHP无法解析该格式,判定会话无效,因此自动触发destroy()销毁会话。 - gc方法返回值违规:根据
SessionHandlerInterface规范,gc()方法需要返回实际清理的会话文件数量,但当前代码返回true,这会导致PHP会话垃圾回收逻辑异常,间接加剧会话销毁问题。 - read方法提前创建空文件:提前创建空文件会让
read()返回空字符串,PHP虽会识别为新会话,但空文件的存在可能干扰后续的会话状态判定。
解决方案
修改read()方法
读取JSON文件后,将其转换为PHP原生会话格式的字符串返回:
public function read(string $id): string { $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . "sess_" . $id . ".json"; if (!file_exists($sessionPath)) { return ''; // 返回空字符串表示新会话,无需提前创建文件 } $jsonData = file_get_contents($sessionPath); // 将JSON反序列化为数组,空文件或无效JSON返回空数组 $rawData = json_decode($jsonData, true) ?: []; // 将数组转换为PHP原生会话格式 $serializedData = ''; foreach ($rawData as $key => $value) { $serializedData .= $key . '|' . serialize($value); } return $serializedData; }
修改gc()方法
统计并返回实际删除的会话文件数量:
public function gc(int $max_lifetime): int|false { $deletedCount = 0; $sessionFiles = glob($this->savePath . DIRECTORY_SEPARATOR . "sess_*" . ".json"); if ($sessionFiles === false) { return 0; // 没有匹配文件返回0 } foreach ($sessionFiles as $file) { if (filemtime($file) + $max_lifetime < time() && file_exists($file)) { unlink($file); $deletedCount++; } } return $deletedCount; }
优化write()方法
避免直接输出异常信息,改用日志记录:
public function write(string $id, string $data): bool { $rawData = []; try { $rawData = self::unserialize_php($data); } catch (Exception $e) { // 记录错误日志,不要直接echo干扰会话流程 error_log("会话序列化失败: " . $e->getMessage()); } $jsonData = json_encode($rawData); $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . 'sess_' . $id . ".json"; return file_put_contents($sessionPath, $jsonData) !== false; }
内容的提问来源于stack exchange,提问作者Alexei Bobrik
相关产品推荐
相关产品推荐

