调试 PHP Session

Tags: php

对于大多数 PHP 开发人员来说,对于 Session 的了解仅是调用 session_start() 方法和 $_SESSION 变量来保存和使用在页面间传递的变量。这是可以理解的,因为 PHP 内置的 Session 支持是非常简单和可靠的。不过,如果出现问题了呢?

了解 Session 是如何工作的对于问题的调试是非常有帮助的。我在 2003 年编写了 Session 真相,这篇文章关注的是一个过时的技术,不够我认为还是有必要读读这篇文章来了解 Session 是如何工作的(仅需要阅读前几部分即可)

不需要费太大的功夫,session_set_save_handler() 可以帮助你,如果你还没有使用过这个函数,手册上的示例是介绍了它基本的特性。另一篇文章 保存 Session 到数据库中 展示了如何将 Session 保存到 MySQL 数据库的方法。现在,重要的是你开始使用自定义的 Session Handler 了,因为这给使得你可以更深入的了解 Session 以及它背后的机制。

如果 session_set_save_handler() 不能工作,别担心,找出问题是一个有意义的练习。如果你使用手册中的示例,需要注意的是这个示例依赖于正确的配置指令,比如 session.save_path

当出现问题时,如果没有一些日志记录发生了什么那么是不可能解决问题的。Session ID 是否发送到客户端了?请求间的 Session ID 是否相同?Session 数据是否正确保存?我可以提出一堆类似的问题,因为这里有太多的环节可能出错了,但是关键是我们需要答案。

让我们看看手册中的 read() 函数吧:

<?php

function read($id)
{
    global $sess_save_path;

    $sess_file = "$sess_save_path/sess_$id";
    return (string) @file_get_contents($sess_file);
}

?>

这里有一些问题:

  • $id 的值是什么?

  • 我们是否可以读取 $sess_file?

  • 返回的是什么数据?

稍微修改以下,我们就可以回答这些问题了:

<?php

function read($id)
{
    global $sess_save_path;

    echo "<p>Session identifier is: {$id}</p>";

    $sess_file = "{$sess_save_path}/sess_{$id}";

    if (is_readable($sess_file)) {
        $data = (string) file_get_contents($sess_file);
        echo "<p>Can read from file: {$sess_file}</p>";
        echo "<p>Data is: {$data}</p>";
    } else {
        echo "<p>Cannot read from file: {$sess_file}</p>";
    }

    return $data;
}

?>

这个方法存在一些问题:

  • 尽管你可能不太在意,不过根据你调试的环境以及访问者,每个 echo 都容易受到 XSS 攻击(在较老版本的 PHP 中并未限制 $id 的数据类型,不过现在不存在了)。如果你介意,那么使用 htmlentities() 或者 htmlspecialchars() 函数。

  • 使用 echo 方法意味这你无法调试 AJAX 请求,API 调用,等。

  • 尽管 open()read(),以及 gc() (按这个顺序执行)在输出流关闭前被执行,不过 write()close() 并不是这样。

手册包含以下注释:

在输出流关闭后 write handler 不会被执行,因此在 write handler 中的调试语句不会输出到浏览器中。如果必须要获得调试输出,建议将调试输出写到文件中。

这是很好的建议,不过如果你确实希望使用 echo,你可以使用 session_write_close() 函数强制结束 Session。如果你编写的是类那么需要在 __destruct() 函数调用这个函数,或者使用 register_shutdown_function() 函数。因为这个原因我会经常使用 session_write_close(),因为我希望获得每个页面的调试输出,这非常有用。

  • 日志的目的是记录发生了什么,单独一个页面无法描绘错误的全貌。

  • 日志捕获所有请求,并非当前页面一个。尤其对 AJAX 请求和 API 请求。

我使用 error_log() 函数来记录日志:

<?php

error_log($message, 3, '/tmp/session.log');

?>

下面是我通常记录的日志示例:

[25 Mar 2011 12:34:56][shiflett.org] [/]

Session::start()
    $_COOKIE['PHPSESSID'] [412e11d5317627e48a4b0615c84b9a8f]
Session::open()
Session::read()
    $id [412e11d5317627e48a4b0615c84b9a8f]
    $data [count|i:1;]
Session::write()
    $id [412e11d5317627e48a4b0615c84b9a8f]
    $data [count|i:2;]
Session::close()

如果你希望 $data 更易读一些,你必须使用 session_decode() 函数,基于以下原因:

  • unserialize() 不同(这个函数不使用,因为数据结构稍微有些区别),session_decode() 函数将结果赋值给 $_SESSION 变量。如果你希望使用这个变量进行调试,你需要自己来保护它。我希望这个函数具备 print_r() 函数一样的可选参数,这样我就有可能获得返回值而不是赋值。

  • 因为 session_decode() 将结果赋值给 $_SESSION 变量,所以在 read() 中无法工作。

你可以使用 PHP 编写自己的 session_decode() 函数(参考手册示例中的 user contributed notes)或者使用 WDDX

<?php

ini_set('session.serialize_handler', 'wddx');

?>

如果你使用 WDDX,你可以使用 wddx_unserialize 函数来反序列化 Session 数据。

请记住在生产环境上调试需要额外的考虑,在这篇文章中我没有涉及。如果可能,最好在别的地方来调试 Session。

我喜欢深入更多细节,由于受到 Ideas of March 鼓舞,我撰写了这篇简短的介绍,并且希望有所帮助。

本文链接:http://www.4byte.cn/learning/83684/diao-shi-php-session.html