检查用户代理并在会话类中重新生成会话ID

时间:2016-01-14 14:07:24

标签: php mysql security session pdo

我编写了一个用于在数据库中存储会话的类。我对我的用户代理检查是否有效感到困惑,因为我无法想到测试它的方法。

我还担心会在每个session_start()上调用session_regenerate_id(),并且被manual's warnings regarding object destruction and the need for session_register_shutdown()混淆。

我的用户代理检查是否始终匹配?

哪里有更新会话ID的好地方?

构造函数是session_register_shutdown()的好地方吗?

提前致谢。

代码:

Session.class.php

<?php
namespace Company\Project;

use \PDO;


class Session
{
    private $dblayer;
    private $user_agent;

    /**
     * Session constructor.
     * @param PDO $dblayer
     */
    public function __construct(PDO $dblayer)
    {
        $this->dblayer = $dblayer;
        $this->user_agent = $_SERVER['HTTP_USER_AGENT'];

        session_set_save_handler(
            array($this, 'open'),
            array($this, 'close'),
            array($this, 'read'),
            array($this, 'write'),
            array($this, 'destroy'),
            array($this, 'gc')
        );

        if ('LIVE' == DEVELOPMENT_MODE) {
            session_set_cookie_params(0, '/', '', true, true);
        } else {
            session_set_cookie_params(0, '/', '', false, true);
        }

        session_register_shutdown();
        session_start();
        session_regenerate_id(true);

    }

    /**
     * @return bool
     */
    public function open()
    {
        if ($this->dblayer) {
            return true;
        }

        return false;
    }

    /**
     * @return bool
     */
    public function close()
    {
        $this->dblayer = null;
        return true;
    }

    /**
     * @param $id
     * @return bool|string
     */
    public function read($id)
    {
        try {
            $this->dblayer->beginTransaction();
            $stmt = $this->dblayer->prepare("SELECT data, user_agent FROM sessions WHERE id = :id LIMIT 1");
            $stmt->bindParam(':id', $id);
            $stmt->execute();

            $this->dblayer->commit();

            if ($row = $stmt->fetch()) {
                $data =  $row['data'];
                $original_user_agent = $row['user_agent'];
            }

            if ($original_user_agent != $this->user_agent) {
                session_destroy();
                header('Location:' . SITE_PATH . '/login.php');
                exit;
            }

            return $data;

        } catch (\Exception $e) {
            $this->dblayer->rollBack();
            // will use file_put_contents to save error message, file etc to error log
            return '';
        }
    }

    /**
     * @param $id
     * @param $data
     * @return bool
     */
    public function write($id, $data)
    {
            $access = time();
            $user_agent = $_SERVER['HTTP_USER_AGENT'];
            $this->dblayer->beginTransaction();
            $stmt = $this->dblayer->prepare("REPLACE INTO sessions VALUES(:id, :data, :user_agent, :access)");
            $stmt->bindParam(':id', $id);
            $stmt->bindParam(':data', $data);
            $stmt->bindParam(':user_agent', $user_agent);
            $stmt->bindParam(':access', $access);
            $stmt->execute();

            $this->dblayer->commit();

            if ($stmt) {
                return true;
            }
            echo 'error';
            $this->dblayer->rollBack();
            // can i save to error log here?
            return false;

    }

    /**
     * @param $id
     * @return bool
     */
    public function destroy($id)
    {
        try {
            $this->dblayer->beginTransaction();
            $stmt = $this->dblayer->prepare("DELETE FROM sessions WHERE id = :id");
            $stmt->bindParam(':id', $id);
            $stmt->execute();

            $this->dblayer->commit();

        } catch (\PDOException $e) {
            $this->dblayer->rollBack();
            // again, will save error data to log
            echo $e->getMessage();
            return false;
        }
    }

    /**
     * @param $max
     * @return bool
     */
    public function gc($max)
    {
        $to_delete = time() - $max;

        try {
            $this->dblayer->beginTransaction();
            $stmt = $this->dblayer->prepare("DELETE FROM sessions WHERE access < :to_delete");
            $stmt->bindParam(':to_delete', $to_delete);

            $this->dblayer->commit();

            return true;

        } catch (\PDOException $e) {
            $this->dblayer->rollBack();
            // save error data to log;
            return false;
        }
    }


}

1 个答案:

答案 0 :(得分:3)

这里有三个问题(至少):

测试您的用户代理检查

要进行测试,您可以更改浏览器用户代理;在Safari中,调试菜单中有几个选项,Chrome和Firefox可能有类似或配置选项,但肯定有插件可以执行此操作。

然而,我建议不要使用这种类型的检查(基本指纹识别) - 不能保证用户代理不会改变 - 如果他们的浏览器在会话中安装更新会发生什么?坦率地说,如果您可以接管用户会话,尤其是重新启动ID会话,则可以欺骗用户用户代理。

在构造函数

中重新生成会话ID

虽然这很好,但在高负载时它可能会对性能产生影响 - 如果你只能在升级权限时重新生成会话ID,那么这是一个更好的选择,例如:

  • 登录时
  • 访问用户个人资料页面时
  • 更改密码时
  • 创建新内容时
  • 结账时
  • 退出时

在构造函数

中使用session_register_shutdown();

虽然手册中说最好在旧版本中使用session_register_shutdown();(或register_shutdown_function('session_write_close')),但实际上我不同意这一点。可以注册多个关闭功能,它们将按照register_shutdown_function()添加的顺序调用 - 但是,如果其中一个调用exit,则不再调用它们。

我认为最好在析构函数中调用session_write_close()

class Session
{
    … 

    public function __destruct()
    {
         session_write_close();
    }
}

即使在调用exit之后,当堆栈被解开时,也会调用它。