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

Posted

技术标签:

【中文标题】检查用户代理并在会话类中重新生成会话 ID【英文标题】:Checking User Agent and regenerating session id in Session Class 【发布时间】:2016-04-19 21:34:04 【问题描述】:

我编写了一个类来将会话存储在数据库中。我不知道我对用户代理的检查是否会起作用,因为我想不出一种方法来测试它。

我还担心每个 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;
        
    



【问题讨论】:

“我不知道我对用户代理的检查是否有效” 你的意思是if ($original_user_agent != $this-&gt;user_agent) ?如果是这样,那张支票让您担心什么? 嗨,对不起,我关心的是动作何时发生,以及在 write 方法中更新数据库是否意味着该值将始终与作为对象属性存储的值匹配。我不确定何时调用这些方法。 session_set_save_handler() documentation中对参数的描述应该会让你有所了解。 如果此代码有效,则它不太适合此站点。试试codereview.stackexchange.com 如何在用户登录时将用户代理存储在_SESSION 数据中,然后在每次页面加载时与存储在_SERVER 中的值进行比较。似乎您将登录逻辑与会话处理内容混合在一起。 【参考方案1】:

你有三个问题(至少):

测试您的用户代理检查

为了测试,您可以更改您的浏览器用户代理;在 Safari 中,Debug 菜单中有一些可用的选项,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 之后,当堆栈被解开时,它也会被调用。

【讨论】:

当依赖__destruct() 方法来处理关闭期间应该调用的函数时要小心,这可能很危险。如果在处理数据库连接等时使用register_shutdown_function(或类似名称)并发生错误,则永远不会调用 destruct 方法并且数据库连接保持打开状态。这就是为什么关键的关闭函数应该在构造函数中注册。根据您的会话(数据库连接等)的管理方式,这可能是一个严重的问题。 这对我来说实际上是一个艰难的决定。您可以使用总是调用的register_shutdown_function(),只要链中没有调用exit,在这种情况下后续函数从不调用。但是,如果发生致命错误(

以上是关于检查用户代理并在会话类中重新生成会话 ID的主要内容,如果未能解决你的问题,请参考以下文章

使用 Session ID 检查会话是不是处于活动状态 - Laravel

检查用户是不是使用会话而不是数据库查询登录

在 Laravel 中检查会话超时

使用会话 ID 取消设置特定会话

在 grails 中维护会话

如何使用 JSTL 检查会话中是不是存在已登录用户?