PHP基础知识程序员华南理工大学无线电爱好者协会软件小组

会话和数据持久存储

2016-11-20  本文已影响57人  honehou

本文为《PHP经典实例》阅读笔记

前言

随着web应用日渐成熟,“有状态性”成为一个常见的需求,有状态应用已经相当普及,甚至被认为是理所当然的。有状态应用是指:访问者浏览网站时,应用能跟踪记录这个访问者的信息。虽然http被设计为无状态协议,不过PHP提供了一组方便的会话管理函数,使实现有状态应用更方便简单,后文将介绍开发有状态应用时要谨记的一些优秀实践做法。

使用会话跟踪

我们可以使用会话模块来跟踪用户,如下面的一个例子:

session_start();
if (! isset($_SESSION['visit'])){
    $_SESSION['visit'] = 0;
}
$_SESSION['visit']++;
echo 'You have visited here '.$_SESSION['visit'].' times.';

会话模块通过向用户发送cookie来跟踪用户,cookie中包含随机生成的session id,且cookie名为PHPSESSID。如果用户不接受cookie,那么会在URL后加上?PHPSESSID=xxxx(id),使之能传递到下一个页面。明显这样的URL并不安全,比如一个用户复制该URL并发送给其他人,那么无意间其他人便会假冒成该用户访问网站,因此默认会禁止这种行为。要启用URL中传递session id的功能,可以在开始会话前使用ini_set('session.use_trans_sid',true)

防止会话劫持

为确保攻击者不能访问另一用户的会话,我们可以规定只允许通过cookie传递session id,并生成另外一个会话token通过URL传递。只有包含一个合法session id和合法token才可以访问会话,如下面部分示例代码:

ini_set('session.use_only_cookies', true);
//指定是否在客户端仅仅使用 cookie 来存放会话 ID,启用此设定可以防止有关通过 URL 传递会话 ID 的攻击。
session_start();
$salt = 'YourSpecialValueHere';
$tokenstr = strval(date('W')).$salt;
$token = md5($tokenstr);

if (!isset($_REQUEST['token']) || $_REQUEST['token'] != $token){
    // 提示登录
    echo "Please login";
    exit;
}

$_SESSION['token'] = $token;
output_add_rewrite_var('token', $token);

该例通过将当前周数strval(date('W'))与变量$salt连接为一个字符串,创建一个自动移位的token,保证token是不固定的且在一段时间内是合法的。
  然后检查请求中的token【$_REQUEST具有$_POST和$_GET的功能,但相对来说会比较慢】,如果未找到则提示重新登录,找到则将它添加到生成的链接【例如当前页面<a>标签的链接后作为get的参数】或者表单中【以input隐藏域形式】,以保证下一次请求能顺利进行。用output_add_rewrite_var()来实现上述功能。

防止会话固定攻击

为确保应用不会受到会话固定攻击(攻击者强制用户使用一个预定义的会话ID),我们应使用会话cookie但会话标识符不会追加到url中,同时频繁生成新的会话ID。

ini_set('session.use_only_cookies', true);
session_start();
if (!isset($_SESSION['generated'])
    || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
}

该例首先设置会话行为,即只能用cookie存储session id,确保PHP不会注意攻击者放在URL中的session id。
  一旦会话开始,设置一个值来记录生成session id的最后时间,定期生成一个新的session id,该例所定时间为30秒,就能大大降低攻击者得到合法session id的几率。
  这两种方法结合,基本可以消除会话固定攻击的风险。攻击者很难得到一个合法的session id,因为id会频繁变化,另外由于session id只能在cookie中传递,因此基于url的攻击是不可能的。

在数据库中存储会话

我们可能希望在数据库中存储会话数据而不是在文件中,这时如果多个服务器可以访问同一个数据库,那么会话数据就会镜像到所有web服务器。具体方法便是通过向session_set_save_handler()提供一个实现SessionHandlerInterface接口的实例,来注册自定义会话存储函数(在PHP 5.4以后的版本才能这样用)。首先我们实现接口如下,其文件名为db.php,它使用PDO将session 数据存储在一个数据库表中:

class DBHandler implements SessionHandlerInterface {
    protected $dbh;
    /**
    * open 回调函数类似于类的构造函数,在会话打开的时候会被调用。 
    * 这是自动开始会话或者通过调用session_start() 手动开始会话 之后第一个被调用的回调函数。 
    * 此回调函数操作成功返回 TRUE,反之返回 FALSE。
    */
    public function open($save_path, $name) {
        try {
            $this->connect($save_path, $name);
            return true;
        } catch (PDOException $e) {
            return false;
        }
    }

    /**
    * close 回调函数类似于类的析构函数。在 write 回调函数调用之后调用。
    * 当调用 session_write_close() 函数之后,也会调用 close 回调函数。
    * 此回调函数操作成功返回 TRUE,反之返回 FALSE。
    */
    public function close() {
        return true;
    }

    /**
    * 销毁session时会调用
    * 当调用session_destroy()函数,或者调用session_regenerate_id()函数并且设置 destroy 参数为 TRUE 时,会调用此回调函数。
    * 此回调函数操作成功返回 TRUE,反之返回 FALSE。
    */
    public function destroy($session_id) {
        $sth = $this->dbh->prepare("DELETE FROM sessions WHERE session_id = ?");
        $sth->execute(array($session_id));
        return true;
    }

    /**
    * 读取session时调用
    * 如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。如果会话中没有数据,read 回调函数返回空字符串。
    * 
    * 在自动开始会话或者通过调用 session_start() 函数手动开始会话之后,PHP内部调用 read 回调函数来获取会话数据。在调用 read 之前,PHP会调用 open 回调函数。
    * 
    * read 回调返回的序列化之后的字符串格式必须与 write 回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $_SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。
    */
    public function read($session_id) {
        $sth = $this->dbh->prepare("SELECT session_data FROM sessions WHERE session_id = ?");
        $sth->execute(array($session_id));
        $rows = $sth->fetchAll(PDO::FETCH_NUM);
        if (count($rows) == 0) {
            return '';
        } else {
            return $rows[0][0];
        }
    }

    /**
    * 向数据库中写入数据
    */
    public function write($session_id, $session_data) {
        $now = time();
        $sth = $this->dbh->prepare("UPDATE sessions SET session_data = ?,last_update = ? WHERE session_id = ?");
        $sth->execute(array($session_data, $now, $session_id));
        if ($sth->rowCount() == 0) {
            $sth2 = $this->dbh->prepare('INSERT INTO sessions (session_id,session_data, last_update)            VALUES (?,?,?)');
            $sth2->execute(array($session_id, $session_data, $now));
        }
    }

    /**
    * 建表
    */
    public function createTable($save_path, $name, $connect = true) {
        if ($connect) {
            $this->connect($save_path, $name);
        }
        $sql=<<<_SQL_
CREATE TABLE sessions (
session_id VARCHAR(64) NOT NULL,
session_data MEDIUMTEXT NOT NULL,
last_update TIMESTAMP NOT NULL,
PRIMARY KEY (session_id)
)
_SQL_;
        $this->dbh->exec($sql);
    }

    /**
    * 连接数据库
    */
    protected function connect($save_path) {
        /* 在DSN中查找作为“查询字符串”参数的用户和密码 */
        $parts = parse_url($save_path);
        if (isset($parts['query'])) {
            parse_str($parts['query'], $query);
            $user = isset($query['user']) ? $query['user'] : null;
            $password = isset($query['password']) ? $query['password'] : null;
            $dsn = $parts['scheme'] . ':';
            if (isset($parts['host'])) {
            $dsn .= '//' . $parts['host'];
            }
            $dsn .= $parts['path'];
            $this->dbh = new PDO($dsn, $user, $password);
        } else {
            $this->dbh = new PDO($save_path);
        }
        $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        // 创建会话表的方法(使用异常处理)
        try {
            $this->dbh->query('SELECT 1 FROM sessions LIMIT 1');
        } catch (Exception $e) {
            $this->createTable($save_path, NULL, false);
        }
    }
}

接下来演示如何将该类与session_set_save_handler()结合,实现在数据库中存储session数据。

include __DIR__ . '/db.php';
ini_set('session.save_path', 'sqlite:/tmp/sessions.db');
session_set_save_handler(new DBHandler);
session_start();
if (! isset($_SESSION['visits'])) {
    $_SESSION['visits'] = 0;
}
$_SESSION['visits']++;
print 'You have visited here '.$_SESSION['visits'].' times.';

这个代码块假设与db.php在同一目录中,一旦将session.save_path设置为指定的PDO DSN,只需要session_set_save_handler(new DBHandler);就可以将PHP与这个程序关联起来。在此之后,使用会话的代码与使用PHP默认处理程序的代码是一样的。

关于以上的函数讲的并不全面,推荐到 http://php.net/ 去查看详情。

上一篇下一篇

猜你喜欢

热点阅读