Sessions in PHP (Advanced Concepts)
Disclaimer: We consider some advanced concepts of PHP sessions here. For basic concepts please check the Sessions section of the PHP Manual. Unfortunately the basic concepts are out of the scope of this article.We consider here:
The HTTP protocol is stateless (so it does not have any built-in mechanism to store variables/array etc. between site page loads). Still in PHP we often need variables/arrays from one site page to be also available at another page of the same site. To make it possible we need to use some special mechanism. Usually sessions or cookies are used.
Let us consider what sessions and cookies are and what is the difference between them.
Since sessions usually use cookies, we would briefly consider cookies first and sessions right after them.
Cookies
Cookies are well explained in the PHP Manual. We would not repeat it here. We would list only some common facts to understand cookie storage limitations.Cookies are stored by the browser in text files on the client side (i.e. right at the user computer). This type of storage has some limitations:
- Storing any sensitive data (e.g. passwords) in cookies is highly not recommended. Anyone who has access to the user computer could extract the data. Because cookies are stored at the user computer hard drive.
- Cookies could be turned off in a browser by the user. So we can not rely on the browser to have cookie functionality available.
- Size of cookies is limited. So you can store only small amounts of data in cookies.
- After being set, cookies would be available in the array $_COOKIE only after the page gets reloaded in the browser.
Cookies are set by the function setcookie() (or by setrawcookie()).
Cookie data are transferred between browser and server by means of HTTP headers. The structure of an HTTP document is strictly defined: first all headers are sent, then a new line, then the body of the HTTP document. It means HTTP headers are always sent at the top of an HTTP document before any other data.
Since setcookie() sends HTTP headers, it must be called before any output is sent to the browser (at least if you do not use Output Buffering).
Simple Example of Setting a Cookie
Let us consider setting a cookie by a subdomain for the main domain and all its subdomains:
Let us set a cookie
'mycookie'
with value '5'
in the file page1.php
located at the subdomain my1
of a domain example.com
(i.e. my1.example.com/page1.php
):
<?php
// No output should go to the browser before this line
// Even a single space would cause a "headers already sent" error
setcookie('mycookie', '5', time() + 3600, '/', '.example.com');
var_dump($_COOKIE['mycookie']); // will output NULL - we need to reload the page to see the cookie
Now let us read the cookie at a page of the main domain
example.com/page2.php
(page2.php is located at the the domain example.com
, not its subdomain):
<?php
print_r($_COOKIE['mycookie']); // outputs '5'
Now let us read the cookie at a page of another subdomain
my2.example.com/page3.php
(page3.php
is located at the subdomain my2
of domain example.com
):
<?php
print_r($_COOKIE['mycookie']); // outputs '5'
To delete a cookie we must use setcookie() with the same parameters as for setting it except we set cookie value to boolean false or empty string and/or expiration date in the past:
setcookie('mycookie', '', time() - 3600, '/', '.example.com');
Sessions
Session storage has 2 sides:- On the client side only the session ID is stored.
- On the server side all the session-related data (variables, arrays etc.) are stored.
Any session is uniquely identified for a user by session name and session ID.
Session Name and Session ID
Session ID could be stored in a cookie (recommended) or transferred between pages as an URL parameter. Transferring the session ID as an URL parameter is not recommended for security reasons.Still the process of passing or not passing session ID in a URL is controlled by the following parameters (this is an example of turning on these parameters in .htaccess):
php_flag session.use_trans_sid Off
php_flag session.use_cookies On
php_flag session.use_only_cookies On
Session name - the default name is "PHPSESSID" (if not defined otherwise with the directive session.name in php.ini, .htaccess, httpd.conf, right in the script or even in the Windows registry).
We can get or set the session name with session_name(). Normally session_name() is called before session_start().
If you are using session_name() to set some particular session name, you need to be very careful:
Basically you could have several sessions with different names in the system. So if at one page you set the session name to some particular value, and at another page forget to do that (which would result in the default name being used) these would be 2 different sessions. And you could wonder for some time why session variables set at one page are not available at the other.
The session name is the name of the cookie variable which will be set at the client computer. As any cookie it is set (by the HTTP server, e.g. Apache) via an HTTP header like:
Set-Cookie: PHPSESSID=42f71a169274754a3f341f107db563cf; path=/
This header tells the browser to set a cookie with the name
"PHPSESSID"
and value "42f71a169274754a3f341f107db563cf"
for the whole domain (path=/)
. No lifetime for this cookie is set in the header. After this the browser sends an HTTP header back to the server (on each page request):
Cookie: PHPSESSID=42f71a169274754a3f341f107db563cf
These examples show that the cookie is being set with the name equal to the session name and the value equal to the session ID.
Session ID - a string value which uniquely identifies the current user session (in the examples above it is equal to
42f71a169274754a3f341f107db563cf
.). We can get or set the session ID with session_id(). Normally session_id() is called before session_start().
If you would like to see how the session cookie is set in your browser, you could do it in the browser Developer Tools. Under Windows in Google Chrome and Mozilla FireFox the Developer Tools panel could be opened by pressing Ctrl+Shift+I on the keyboard. Then you could go to tab
Network
, click on any file on the left and click on tabs Headers
or Cookies
which would get available.In the earlier versions of Mozilla FireFox the add-on Live HTTP Headers was very popular for viewing HTTP headers.
Session Lifetime
The default session cookie lives indefinitely till the browser gets closed (explained below).The session lifetime, in this case, is solely defined by the session.gc_maxlifetime setting.
But if several systems work on the same server and store session data in the same place, the script with minimum session.gc_maxlifetime will clean session data for all systems. Please see this note.
By default PHP stores session data in the file system. With a file-based session handler, files are deleted by the garbage collector by their access times (atime). So it works only if the file system keeps track of access times (e.g. Windows FAT did not).
Also since the garbage collector is run with some probability, a session with a session cookie (lifetime=0) could live longer than session.gc_maxlifetime defines. In Debian/Ubuntu the garbage collector works by Cron. Please also see below for more details.
Session Lifetime on the Client Side
Session lifetime on the client side (in browser) is defined by the session cookie lifetime. Which is defined by the settingsession.cookie_lifetime
in php.ini. By default, it is equal to 0 which means "until the browser gets closed". But you could set the session cookie lifetime to some fixed value (in seconds).Please note: Setting nonzero value for
session.cookie_lifetime
could be not safe. This is because the session cookie will live even after all browser windows are closed (for the given period of time). If despite this warning you still decide to set
session.cookie_lifetime
to a nonzero value, you could do it in php.ini, .htaccess, httpd.conf, right in the script or even in the Windows registry. E.g. in .htaccess you could set it like:
php_value session.cookie_lifetime 3600
which would make the session cookie lifetime equal to 1 hour. Session Lifetime on the Server Side
By default, PHP session data is stored in files at the server (you could store session data in a database of course - please see the sections right below).When the session expires, these files should be deleted by the garbage collector.
The garbage collector is run randomly on the session start. It means if the site has very few visitors, the files with session data could be left at the server for a very long time. Despite the session has expired long ago.
How the garbage collector is run is defined by the following constants in php.ini:
session.gc_maxlifetime
- defines the number of seconds after which the session data at the server are considered "garbage". The default value for
session.gc_maxlifetime
is 1440. Which means the session data are considered "garbage" after 24 minutes.session.gc_probability
and session.gc_divisor
together define the probability with which the garbage collector is run on session start. The probability would be equal to session.gc_probability/session.gc_divisor
(e.g. 1/100=0.01).All 3 constants could be redefined in php.ini, .htaccess, httpd.conf, right in the script or under Windows even in the Windows registry.
E.g.
session.gc_maxlifetime
could be redefined in .htaccess as:php_value session.gc_maxlifetime 18000
which would set the maximum lifetime of the session at the server to 5 hours (=1800 seconds).In Debian/Ubuntu operating systems this default PHP mechanism of garbage collector work is turned off. The garbage collector is run by Cron.
E.g. under Ubuntu 18.04 session.gc_probability is set to 0 in php.ini. And as it could be seen in
/etc/cron.d/phpthe garbage collector is run every 30 minutes by Cron.
Storing Session Data in a Database
By default session data is stored in files. This is not considered the most efficient mechanism and used mostly on low load sites. A better practice is to store session data in a database. Which is considered below.Storing Session Data in Redis
Redis is a key/value NoSQL database which is very performance-oriented. Due to the database high efficiency, it is good for storing session data.The instructions below are given for Ubuntu 14.04.
Please note: You are supposed to be an experienced Ubuntu user or system administrator since you would need to assemble a few things from sources here.
To store session data in Redis first you need to install Redis at your server.
sudo apt-get update sudo apt-get install redis-serverPersonally I prefer to assemble it from sources (could be recommended for experienced Ubuntu users only!) as described here and here (please substitute Redis version in the articles with the version you need). It could be done to get more recent version of Redis than available from the standard Ubuntu repository.
Also I normally set these 3 options in /etc/redis/redis.conf:
bind 127.0.0.1 databases 1 appendonly yesand restart the redis server.
Please note: While everything should work without the 2nd and 3rd options, the bind option
bind 127.0.0.1is really necessary. Redis is open for external connections by default. So without this bind option, anyone on the Internet would be able to connect to your Redis database and not only read your users session data (and any other data you ever store in Redis) but also change this data in any way. Of course it is a very good idea apart from the bind option also to close all the incoming ports with a firewall. Under Ubuntu ufw could be a simple choice (and a pretty reliable as well since it is a front-end to iptables).
Now you have Redis but you have no means to connect to it from PHP. To make it possible to work with Redis from PHP you could use e.g. phpredis (this library is assembled from sources as described here).
After this you could make PHP store session data in Redis as described here. On a single server I normally do it in php.ini like this:
session.save_handler = redis session.save_path = "tcp://localhost:6379?weight=1"Do not forget to restart php5-fpm (if you use Nginx) or Apache (if PHP is used as a module) for the php.ini changes to take place.
As we discussed before, session data at an Ubuntu 14.04 server is deleted by the garbage collector run by Cron every 30 miuntes. With sessions in Redis, this data seems to be cleaned exactly (almost exactly - about 1 second delay is still possible) after session.gc_maxlifetime seconds have passed. Apparently Redis expire() or expireAt() function is used by the session handlers to do that (Redis has this built-in feature to expire keys after given number of seconds if one of these functions is used).
Storing Session Data in Memcached
Simple instructions for Ubuntu 14.04 could be like this:sudo su apt-get update apt-get install memcached apt-get install php5-memcache
Make sure that in the file /etc/memcached.conf the line
-l 127.0.0.1is NOT commented.
Now enable session Memcached handler in php.ini:
session.save_handler = memcache session.save_path = "tcp://localhost:11211"
And if Apache with PHP installed as a module used, restart Apache:
service apache2 restartOr if Nginx with php5-fpm is used, restart the php5-fpm service:
service php5-fpm restart
Also you could read this very good article about Memcached installation under Ubuntu 12.04.
Please note: Normally all data in Memcached is kept in RAM and does not persist on server reboot. So if you use Memcahed session handler, please be ready to lose your session data on server reboot (e.g. all your logged in users could get logged out suddenly).
Storing Session Data in MySQL
To store session data in MySQL you need to implement custom session handlers in PHPA very simple example of MySQL session handler is given here. It is given just to make it possible to grasp the idea. But in your system you would definitely need a solution more complicated than this.
Also you would need at least PHP 5.4 to run the example. Before this version it was necessary to create separate handler functions and register them with session_set_save_handler() instead of implementing the interface SessionHandlerInterface.
First create a table in MySQL to store session data:
CREATE TABLE `session` (
`id` CHAR(32) NOT NULL,
`data` BLOB,
`time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
The following script contains a class implementing SessionHandlerInterface. It defines a simple MySQL-based session handler. The script must be included at the very top of every page where sessions are necessary:
<?php
// These lines should be somewhere in a config file
define('DB_HOST', 'localhost');
define('DB_USER', 'myusername');
define('DB_PASSWORD', 'mypassword');
define('DB_DATABASE', 'mydatabase');
// Define a class
class MySqlSessionHandler implements SessionHandlerInterface {
protected $_mysqli;
/**
* Initializes session. In our simple example just opens th database connection here.
*
* @param string $savePath - the path to store/retrive the session - we ignore it here.
* @param string $sessionName - session name - in this simple example we ignore it too.
* @return boolean true on success or false on failure
*/
function open($savePath, $sessionName) {
$this->_mysqli = @new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
if($this->_mysqli->connect_error) {
return false; // you would probably want to add some error logging here
}
return true;
}
/**
* Normally closes the current session.
* In this simple example we are just closing the previously opened database connection.
*
* @return boolean true on success or false on error.
*/
public function close() {
return $this->_mysqli->close();
}
/**
* Writes session data $sessionData to a row identified by $sessionId.
*
* @param string $sessionId - session id
* @param string $sessionData - session data (already serialized by PHP)
* @return boolean true on success or false on error.
*/
public function write($sessionId, $sessionData) {
$sessionId = $this->_mysqli->real_escape_string($sessionId);
$sessionData = $this->_mysqli->real_escape_string($sessionData);
$q = "REPLACE INTO session (id, data) VALUES ('$sessionId', '$sessionData')";
// you could probably want to add logging of $mysqli->error here on false before returning the result
return $this->_mysqli->query($q);
}
/**
* We read from MySQL data which has been written there by method write().
*
* @param string $sessionId - session id
* @return string - a string of data found in the database by $sessionId
* (empty string if nothing has been found)
*/
public function read($sessionId) {
$q = "SELECT data FROM session WHERE id = '" . $this->_mysqli->real_escape_string($sessionId) . "'";
$res = $this->_mysqli->query($q);
if(false === $res) {
return ''; // you would probably want to add logging of $mysqli->error here
}
$row = $res->fetch_assoc();
$res->free();
return (string) $row['data'];
}
/**
* Destroys a session.
* In our case, deletes a row from the table `session` uniquely identified by session id $sessionId.
*
* @param string $sessionId - session id
* @return boolean true on success or false on error.
*/
public function destroy($sessionId) {
$q = "DELETE FROM session WHERE id='" . $this->_mysqli->real_escape_string($sessionId) . "'";
// you could probably want to add logging of $mysqli->error here on false before returning the result
return $this->_mysqli->query($q);
}
/**
* Cleans up expired sessions.
*
* @param string $maxlifetime - sessions that have not updated for the last maxlifetime seconds
* will be removed
* @return boolean true on success or false on error.
*/
public function gc($maxlifetime) {
$maxlifetime = (int) $maxlifetime;
$q = "DELETE FROM session WHERE DATE_ADD(`time`, INTERVAL $maxlifetime SECOND)<NOW()";
// you could probably want to add logging of $mysqli->error here on false before returning the result
return $this->_mysqli->query($q);
}
}
session_set_save_handler(new MySqlSessionHandler, true);
session_start();
Of course since session_start() sends HTTP headers (since session cookie as any cookie is sent via HTTP headers), this script must be executed before any output has been sent to the browser. Or (with a cookie-based session and without Output Buffering) you would get "headers already sent" error.
You can find more of my articles here.