我已经在这堂课上工作了一段时间,希望能得到一些反馈,即使只是挑剔。我在下面添加了有关如何使用该类的基本说明以及一些问题。

<?php
/**
 * @category    Security
 * @version     1.0
 * @author      First Last <email@mail.com>
 * */
class SessionHandler
{
    /**
     * @description PDO database handle
     * @access private
     * @var PDO database resource
     * */
    private $_db                        = NULL;

    /**
     * @description Database table name where sessions are stored.
     * @access private
     * @var string
     * */
    private $_table_name                = 'sessions';

    /**
     * @description Cookie name where the session ID is stored.
     * @access private
     * @var string
     * */
    private $_cookie_name               = 'session_cookie';

    /**
     * @description Number of seconds before the session expires. Default is 2 hours.
     * @access private
     * @var integer
     * */
    private $_seconds_till_expiration   = 7200; // 2 hours

    /**
     * @description Number of seconds before the session ID is regenerated. Default is 5 minutes.
     * @access private
     * @var integer
     * */
    private $_renewal_time              = 300; // 5 minutes

    /**
     * @description Closes the session when the browser is closed.
     * @access private
     * @var boolean
     * */
    private $_expire_on_close           = FALSE;

    /**
     * @description IP address that will be checked against the database if enabled. Must be a valid IP address.
     * @access private
     * @var string
     * */
    private $_ip_address                = FALSE;

    /**
     * @description User agent that will be checked against the database if enabled.
     * @access private
     * @var string
     * */
    private $_user_agent                = FALSE;

    /**
     * @description Will only set the session cookie if a secure HTTPS connection is being used.
     * @access private
     * @var boolean
     * */
    private $_secure_cookie             = FALSE;

    /**
     * @description A hashed string which is the ID of the session.
     * @access private
     * @var string
     * */
    private $_session_id                = '';

    /**
     * @description Data stored by the user.
     * @access private
     * @var array
     * */
    private $_data                      = array();

    /**
     * @description Initializes the session handler.
     * @access public
     * @param array - configuration options
     * @return void
     * */
    public function __construct(array $config)
    {
        // Sets user configuration
        $this->_setConfig($config);

        // Runs the session mechanism
        if ($this->_read()) {
            $this->_update();
        } else {
            $this->_create();
        }

        // Cleans expired sessions if necessary and writes cookie
        $this->_cleanExpired();
        $this->_setCookie();
    }

    /**
     * @description Regenerates a new session ID for the current session.
     * @access public
     * @return void
     * */
    public function regenerateId()
    {
        // Acquires a new session ID
        $old_session_id     = $this->_session_id;
        $this->_session_id  = $this->_generateId();

        // Updates session ID in the database
        $stmt = $this->_db->prepare("UPDATE {$this->_table_name} SET time_updated = ?, session_id = ? WHERE session_id = ?");
        $stmt->execute(array(time(), $this->_session_id, $old_session_id));

        // Updates cookie
        $this->_setCookie();
    }

    /**
     * @description Sets a specific item to the session data array.
     * @access public
     * @param string - session data array key
     * @param string - data value
     * @return void
     * */
    public function setData($key, $value)
    {
        $this->_data[$key] = $value;
        $this->_write(); // Writes to database
    }

    /**
     * @description Unsets a specific item from the session data array.
     * @access public
     * @param string - session data array key
     * @return void
     * */
    public function unsetData($key)
    {
        if (isset($this->_data[$key])) unset($this->_data[$key]);
    }

    /**
     * @description Returns a specific item from the session data array.
     * @access public
     * @param string - session data array key
     * @return string - data value/FALSE
     * */
    public function getData($key)
    {
        return isset($this->_data[$key]) ? $this->_data[$key] : FALSE;
    }

    /**
     * @description Returns all items in the session data array.
     * @access public
     * @return array
     * */
    public function getAllData()
    {
        return $this->_data;
    }

    /**
     * @description Destroys the current session.
     * @access public
     * @return void
     * */
    public function destroy()
    {
        // Deletes session from the database
        if (isset($this->_session_id))
        {
            $stmt = $this->_db->prepare("DELETE FROM {$this->_table_name} WHERE session_id = ?");
            $stmt->execute(array($this->_session_id));
        }

        // Kills the cookie
        setcookie(
            $this->_cookie_name,
            '',
            time() - 31500000,
            NULL,
            NULL,
            NULL,
            NULL
        );
    }

    /**
     * @description The main session mechanism:
     *      - Reads session cookie and retrives session data
     *      - Checks session expiration
     *      - Verifies IP address (if enabled)
     *      - Verifies user agent (if enabled)
     * @access private
     * @return void
     * */
    private function _read()
    {
        // Fetches session cookie
        $session_id = isset($_COOKIE[$this->_cookie_name]) ? $_COOKIE[$this->_cookie_name] : FALSE;

        // Cookie doesn't exist!
        if (! $session_id) {
            return FALSE;
        }

        $this->_session_id = $session_id;

        // Fetches the session from the database
        $stmt = $this->_db->prepare("SELECT data, time_updated, user_agent, ip_address FROM {$this->_table_name} WHERE session_id = ?");
        $stmt->execute(array($this->_session_id));

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        // Did a session exist?
        if ($result !== FALSE && count($result) > 0)
        {
            // Checks if the session has expired in the database
            if (! $this->_expire_on_close)
            {
                if (($result['time_updated'] + $this->_seconds_till_expiration) < time())
                {
                    $this->destroy();
                    return FALSE;
                }
            }

            // Checks if the user's IP address matches the one saved in the database
            if ($this->_ip_address)
            {
                if ($result['ip_address'] != $this->_ip_address)
                {
                    $this->_flagForUpdate();
                    return FALSE;
                }
            }

            // Checks if the user's user agent matches the one saved in the database
            if ($this->_user_agent)
            {
                if ($result['user_agent'] != $this->_user_agent)
                {
                    $this->_flagForUpdate();
                    return FALSE;
                }
            }

            // Checks if the session has been requested to regenerate a new ID (hack attempt)
            $this->_checkUpdateFlag();

            // Checks if the session ID needs to be renewed (time exceeded)
            $this->_checkIdRenewal();

            // Sets user data
            $user_data = unserialize($result['data']);

            if ($user_data) {
                $this->_data = $user_data;
                unset($user_data);
            }

            // All good!
            return TRUE;
        }

        // No session found
        return FALSE;
    }

    /**
     * @description Creates a session.
     * @access private
     * @return void
     * */
    private function _create()
    {
        // Generates session ID
        $this->_session_id = $this->_generateId();

        // Inserts session into database
        $stmt = $this->_db->prepare("INSERT INTO {$this->_table_name} (session_id, user_agent, ip_address, time_updated) VALUES (?, ?, ?, ?)");
        $stmt->execute(array($this->_session_id, $this->_user_agent, $this->_ip_address, time()));
    }

    /**
     * @description Updates a current session.
     * @access private
     * @return void
     * */
    private function _update()
    {
        // Updates session in database
        $stmt = $this->_db->prepare("UPDATE {$this->_table_name} SET time_updated = ? WHERE session_id = ?");
        $stmt->execute(array(time(), $this->_session_id));
    }

    /**
     * @description Writes session data to the database.
     * @access private
     * @return void
     * */
    private function _write()
    {
        // Custom data doesn't exist
        if (count($this->_data) == 0) {
            $custom_data = '';
        } else {
            $custom_data = serialize($this->_data);
        }

        // Writes session data to database
        $stmt = $this->_db->prepare("UPDATE {$this->_table_name} SET data = ?, time_updated = ? WHERE session_id = ?");
        $stmt->execute(array($custom_data, time(), $this->_session_id));
    }

    /**
     * @description Sets session cookie.
     * @access private
     * @return void
     * */
    private function _setCookie()
    {
        setcookie(
            $this->_cookie_name,
            $this->_session_id,
            ($this->_expire_on_close) ? 0 : time() + $this->_seconds_till_expiration, // Expiration timestamp
            NULL,
            NULL,
            $this->_secure_cookie, // Will cookie be set without HTTPS?
            TRUE // HttpOnly
        );
    }

    /**
     * @description Removes expired sessions from the database.
     * @access private
     * @return void
     * */
    private function _cleanExpired()
    {
        // 0.1 % chance to clean the database of expired sessions
        if (mt_rand(1, 1000) == 1)
        {
            $stmt = $this->_db->prepare("DELETE FROM {$this->_table_name} WHERE (time_updated + {$this->_seconds_till_expiration}) < ?");
            $stmt->execute(array(time()));
        }
    }

    /**
     * @description Creates a unique session ID.
     * @access private
     * @return string
     * */
    private function _generateId()
    {
        $salt                   = 'x7^!bo3p,.$$![&Q.#,//@i"%[X';
        $random_number          = mt_rand(0, mt_getrandmax());
        $ip_address_fragment    = md5(substr($_SERVER['REMOTE_ADDR'], 0, 5));
        $timestamp              = md5(microtime(TRUE) . time());

        $hash_data = $random_number . $ip_address_fragment . $salt . $timestamp;
        $hash = hash('sha256', $hash_data);

        return $hash;
    }

    /**
     * @description Checks if the session ID needs to be regenerated and does so if necessary.
     * @access private
     * @return void
     * */
    private function _checkIdRenewal()
    {
        // Gets the last time the session was updated
        $stmt = $this->_db->prepare("SELECT time_updated FROM {$this->_table_name} WHERE session_id = ?");
        $stmt->execute(array($this->_session_id));

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($result !== FALSE && count($result) > 0)
        {
            // Checks if the session ID has exceeded it's permitted lifespan.
            if ((time() - $this->_renewal_time) > $result['time_updated'])
            {
                // Regenerates a new session ID
                $this->regenerateId();
            }
        }
    }

    /**
     * @description Flags a session so that it will receive a new ID on the next subsequent request.
     * @access private
     * @return void
     * */
    private function _flagForUpdate()
    {
        $stmt = $this->_db->prepare("UPDATE {$this->_table_name} SET flagged_for_update = '1' WHERE session_id = ?");
        $stmt->execute(array($this->_session_id));
    }

    /**
     * @description Checks if the session has been requested to regenerate a new ID and does so if necessary.
     * @access private
     * @return void
     * */
    private function _checkUpdateFlag()
    {
        // Gets flagged status
        $stmt = $this->_db->prepare("SELECT flagged_for_update FROM {$this->_table_name} WHERE session_id = ?");
        $stmt->execute(array($this->_session_id));

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($result !== FALSE && count($result) > 0)
        {
            // Flagged?
            if ($result['flagged_for_update'])
            {
                // Creates a new session ID
                $this->regenerateId();

                // Updates database
                $stmt = $this->_db->prepare("UPDATE {$this->_table_name} SET flagged_for_update = '0' WHERE session_id = ?");
                $stmt->execute(array($this->_session_id));
            }
        }
    }

    /**
     * @description Sets configuration.
     * @access private
     * @param array - configuration options
     * @return void
     * */
    private function _setConfig(array $config)
    {
        // Sets database handle
        if (isset($config['database'])) {
            $this->_db = $config['database'];
        } else {
            throw new Exception('Database handle not set!');
        }

        // --------------------------------------------

        // Cookie name
        if (isset($config['cookie_name']))
        {
            // Checks if alpha-numeric
            if (! ctype_alnum(str_replace(array('-', '_'), '', $config['cookie_name']))) {
                throw new Exception('Invalid cookie name!');
            }

            $this->_cookie_name = $config['cookie_name'];
        }

        // --------------------------------------------

        // Database table name
        if (isset($config['table_name']))
        {
            // Checks if alpha-numeric
            if (! ctype_alnum(str_replace(array('-', '_'), '', $config['table_name']))) {
                throw new Exception('Invalid table name!');
            }

            $this->_table_name = $config['table_name'];
        }

        // --------------------------------------------

        // Expiration time in seconds
        if (isset($config['seconds_till_expiration']))
        {
            // Anything else than digits?
            if (! is_int($config['seconds_till_expiration']) || ! preg_match('#[0-9]#', $config['seconds_till_expiration'])) {
                throw new Exception('Seconds till expiration must be a valid number.');
            }

            // Negative number or zero?
            if ($config['seconds_till_expiration'] < 1) {
                throw new Exception('Seconds till expiration can not be zero or less. Enable session expiration when the browser closes instead.');
            }

            $this->_seconds_till_expiration = (int) $config['seconds_till_expiration'];
        }

        // --------------------------------------------

        // End the session when the browser is closed?
        if (isset($config['expire_on_close']))
        {
            // Not TRUE or FALSE?
            if (! is_bool($config['expire_on_close'])) {
                throw new Exception('Expire on close must be either TRUE or FALSE.');
            }

            $this->_expire_on_close = $config['expire_on_close'];
        }

        // --------------------------------------------

        // How often should the session be renewed?
        if (isset($config['renewal_time']))
        {
            // Anything else than digits?
            if (! is_int($config['renewal_time']) || ! preg_match('#[0-9]#', $config['renewal_time'])) {
                throw new Exception('Session renewal time must be a valid number.');
            }

            // Negative number or zero?
            if ($config['renewal_time'] < 1) {
                throw new Exception('Session renewal time can not be zero or less.');
            }

            $this->_renewal_time = (int) $config['renewal_time'];
        }

        // --------------------------------------------

        // Check IP addresses?
        if (isset($config['check_ip_address']))
        {
            // Not a string?
            if (! is_string($config['check_ip_address'])) {
                throw new Exception('The IP address must be a string similar to this: \'172.16.254.1\'.');
            }

            // Invalid IP?
            if (! preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/', $config['check_ip_address'])) {
                throw new Exception('Invalid IP address.');
            }

            $this->_ip_address = $config['check_ip_address'];
        }

        // --------------------------------------------

        // Check user agent?
        if (isset($config['check_user_agent'])) {
            $this->_user_agent = substr($config['check_user_agent'], 0, 999);
        }

        // --------------------------------------------

        // Send cookie only when HTTPS is enabled?
        if (isset($config['secure_cookie']))
        {
            if (! is_bool($config['secure_cookie'])) {
                throw new Exception('The secure cookie option must be either TRUE or FALSE.');
            }

            $this->_secure_cookie = $config['secure_cookie'];
        }
    }
}


这里是使用该类的方式。请注意,我是如何注释掉配置选项的,这是因为默认情况下,所有这些选项都是在内部设置的,以便在注入配置之外给我提供更整洁的配置,以便我可以省略它们。我还注入了IP / UA,以便可以以适当的方式获取它,而不是会话类的工作。

<?php

// Database connection
$pdo = new PDO('mysql:host=localhost;dbname=security', 'root', '', array(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING));

// -----------------------------------------------------------------------------------------

// Loads session handler
include_once('classes/session-class.php');

// SESSION HANDLER CONFIGRATION
$config['database']                 = $pdo;             // PDO Database handle

/*
$config['cookie_name']              = 'session_cookie'; // Name of the cookie
$config['table_name']               = 'sessions';       // Database table name
$config['seconds_till_expiration']  = 7200;             // How many seconds it takes before the session expires. Default is 2 hours.
$config['renewal_time']             = 300;              // How many seconds it takes before the session ID is renewed. Default is 5 minutes.
$config['expire_on_close']          = FALSE;            // The session is terminated when the browser is closed.
$config['secure_cookie']            = FALSE;            // Decides whether the cookie should only be set when a HTTPS connection exists.
$config['check_ip_address']         = $_SERVER['REMOTE_ADDR'];        // Will check the user's IP address against the one stored in the database. Make sure this is a string which is a valid IP address. FALSE by default.
$config['check_user_agent']         = $_SERVER['HTTP_USER_AGENT'];    // Will check the user's user agent against the one stored in the database. FALSE by default.
*/

try
{
    $session = new SessionHandler($config);
}
catch (Exception $e) {
    echo $e->getMessage();
    exit;
}


问题:


如何改进现有代码?
是否存在明显的安全性或逻辑缺陷?
文档化/注释性足够好或范围太广?
对于字符串形式的变量,将被设置:使用FALSENULL作为默认值? (例如$_user_agent = FALSENULL;
检查有效整数的更好方法?
_setConfig()方法。讨厌吗?将其拆分为更小的方法毫无意义吗?
注释类变量是否有好处?

@param void@return void不必要吗?
在数据库中为值省略单引号是否安全?用准备好的语句查询? ?'?'


评论

这个问题有很多看法,所以我假设人们正在使用您的代码。您愿意在编辑中发布完成的课程吗?

我已经好多年没有看过这段代码了,建议任何打算使用它的人在这样做之前都要仔细阅读它。我退出了这个项目,转而开始一个更具模块化和单元测试的项目,但是从阅读中我无法验证它是否比该生产方式更稳定,因为我不久之后就离开了php,因此我决定不发布该项目。 。这里的代码可能会按预期工作,但是我不建议任何人使用此未经测试的代码。这里不要将代码作为良好编码的示例,而应作为学习参考。

感谢更新。看来您可能已经准备好将其公开发布,这就是我问的原因。
您的代码接近完美!

@ n01ze我不同意。此类不过是数据库操作的基础,因此将它们全部单独发送而不是作为事务发送(可能的话)。它还有太多的责任,因此调试起来很麻烦(是的,我仍然记得!)。最重要的是,这导致了细微而危险的错误,这些错误可能会完全损害会话机制的整个安全性(是的,我也记得这一点,那太糟糕了!)。正如我上面提到的;仅供学习参考,请勿将此作为良好代码的示例。

#1 楼

您的代码看起来很好。我喜欢您使用依赖注入的方式。这段代码是其他人可以效仿的一个很好的例子。

这里只有几点,因为没有其他人回答了。


1。如何改善现有代码?


从构造函数中移出构造对象时所做的工作。 MiškoHevery说的比我在这里说的更好。基本上,这使您难以测试或扩展对象。我要做的是用setConfig方法代码替换构造函数,因为它正在检查正在创建的对象的配置(这在构造对象时是一件好事)。

我会重写它首先抛出所有检查和异常,然后在构造函数的末尾分配所有字段。

我将从构造函数中移出以下内容:

// Runs the session mechanism
if ($this->_read()) {
    $this->_update();
} else {
    $this->_create();
}


并使其成为一个公共方法,该方法将在构造对象后调用。我不确定你会怎么称呼它。在我看来,整个事情都可以命名为update,因为它可以更新或创建会话。

我还将cleanExpired和setCookie移出了构造函数。我不确定cleanExpired是否确实属于此类。至少使用它作为单独的方法,您可以更好地控制调用它的时间。


非常小:考虑将这些方法按字母顺序排列在类中。

非常轻微:我认为1个if语句优于2个,请考虑更改以下内容:

    if (! $this->_expire_on_close)
    {
        if (($result['time_updated'] + $this->_seconds_till_expiration) < time())


至:

   if (! $this->_expire_on_close &&
       (($result['time_updated'] + $this->_seconds_till_expiration) < time()))


逻辑被分组在一起,并且可读性强。


2.任何明显的安全性或逻辑缺陷?


我看不到明显的东西,但是我看上去并不努力,也不是安全专家。我建议您检查堆栈溢出时的密码盐化答案,以获取有关此方面的具体建议。


3.记录/注释是否足够充分或足够广泛?


看起来不错,很高兴看到docblock注释。


4.对于将被设置为字符串的变量:使用FALSE还是NULL作为默认值? (例如$ _user_agent = FALSE vs NULL;)


我的首选是NULL。它使检查看起来更自然:

   if (isset($_user_agent) &&
   // vs the more magical / less self explanatory.
   if (($_user_agent) &&



5.检查有效整数的更好方法?


我听说PHP过滤器功能可用于此类事情。


6. _setConfig()方法。讨厌吗?将其拆分为更小的方法毫无意义吗?


请参见1.我不会将其拆分。


7.这样做有什么好处吗?注释类变量?


由您决定。是否需要@access?我以为是自动生成的吗?


8. @param无效和@return无效吗?


是的,我认为它们是不必要的。


9.在带有准备好的语句的数据库查询中,为值省略单引号是否安全? ? vs'?'。


是的?是占位符,尝试使用“?”会使其成为一个值,查询将不再起作用(我认为)。

评论


\ $ \ begingroup \ $
自从发布评论以来,我对课程的修改很小,而且很有趣,我已经按照建议的大多数方式对其进行了更改。
\ $ \ endgroup \ $
–概化
2012年2月5日,下午1:41

\ $ \ begingroup \ $
如何存储和回显会话值。一个小的文档对我尝试<?php include_once('classes / session-class.php');很有帮助。 session_start(); $ session =新的mySessionHandler($ config); //将您的代码放在这里echo $ config ['cookie_name']; ?>这不起作用
\ $ \ endgroup \ $
–sanoj劳伦斯
18年7月10日在20:32



\ $ \ begingroup \ $
您需要查看SessionHandler类的公共方法(而不是mySessionHandler)。看起来setData和getData方法可以提供帮助。
\ $ \ endgroup \ $
– Paul
18年7月10日在23:27

\ $ \ begingroup \ $
@Paul,当我重新启动浏览器时,它不会更新,因此会在数据库中创建一个新会话。
\ $ \ endgroup \ $
–sanoj劳伦斯
18年7月22日在6:36

\ $ \ begingroup \ $
也许expire_on_close的配置选项是@Seralize,它是编写代码的人。他们会更好。
\ $ \ endgroup \ $
– Paul
18年7月23日在6:58

#2 楼

关于样式的要点...在所有语言中,我一直考虑使用下划线前缀来表示保留字,常量等(PHP将其用于$ _SESSION,$ _ GET等,这符合此约定)。

因此,如果您必须使用一个下划线,最好在类成员中使用。