<?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;
}
问题:
如何改进现有代码?
是否存在明显的安全性或逻辑缺陷?
文档化/注释性足够好或范围太广?
对于字符串形式的变量,将被设置:使用
FALSE
或NULL
作为默认值? (例如$_user_agent = FALSE
与NULL;
)检查有效整数的更好方法?
_setConfig()
方法。讨厌吗?将其拆分为更小的方法毫无意义吗?注释类变量是否有好处?
@param void
和@return void
不必要吗?在数据库中为值省略单引号是否安全?用准备好的语句查询?
?
与'?'
。#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等,这符合此约定)。因此,如果您必须使用一个下划线,最好在类成员中使用。
评论
这个问题有很多看法,所以我假设人们正在使用您的代码。您愿意在编辑中发布完成的课程吗?我已经好多年没有看过这段代码了,建议任何打算使用它的人在这样做之前都要仔细阅读它。我退出了这个项目,转而开始一个更具模块化和单元测试的项目,但是从阅读中我无法验证它是否比该生产方式更稳定,因为我不久之后就离开了php,因此我决定不发布该项目。 。这里的代码可能会按预期工作,但是我不建议任何人使用此未经测试的代码。这里不要将代码作为良好编码的示例,而应作为学习参考。
感谢更新。看来您可能已经准备好将其公开发布,这就是我问的原因。
您的代码接近完美!
@ n01ze我不同意。此类不过是数据库操作的基础,因此将它们全部单独发送而不是作为事务发送(可能的话)。它还有太多的责任,因此调试起来很麻烦(是的,我仍然记得!)。最重要的是,这导致了细微而危险的错误,这些错误可能会完全损害会话机制的整个安全性(是的,我也记得这一点,那太糟糕了!)。正如我上面提到的;仅供学习参考,请勿将此作为良好代码的示例。