You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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时,会话文件再次被销毁,跳回登录页。

核心原因

  1. 会话格式不匹配:PHP会话系统要求read()方法返回PHP原生序列化格式的字符串(即key|serialized_value格式),但当前read()直接返回JSON字符串,PHP无法解析该格式,判定会话无效,因此自动触发destroy()销毁会话。
  2. gc方法返回值违规:根据SessionHandlerInterface规范,gc()方法需要返回实际清理的会话文件数量,但当前代码返回true,这会导致PHP会话垃圾回收逻辑异常,间接加剧会话销毁问题。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 12:10:57