Bubble.ro - because there is always something new to learn about

Creating a customized session handling system in PHP (part II)

 

Category: Programming/PHP

In the first part of this article I've explained the basics of sessions and how they can be used. It is now time to see how we can implement these theories and create a functional session management system using a database.

I will use PHP 4 sessions to make all functions well organized and easy to reuse and for the database system MySQL, although the database interface will be created as a separate class so it can be modified according to everyone's needs.

The SQL structure

Here is a sample SQL structure I've used for this example. The ses_id is the session id (sid) which is also the primary key for the table. I used an INT(11) for it managed manually through the script, again this is just my choice, I've explain some others in the previous part.

The start_time and last_time represent time when the session was created (time of the first page load for this user) and the time of the last page load. Both are stored using UNIX timestamps - because integer operations are generally faster and easier to use.

I've decided to identify an unique session by the ses_id (if available), the user's IP and his browser agent. User's IP address is stored in the user_ip field and user_agent contains the agent information (truncated to 255 characters).

I've added some other fields as well for user identification. The field logged_in shows if a user is logged in or not (1 = logged in, 0 = not logged in) - for this field you can use an ENUM('0', '1'), I've chosen TINYINT for simplity. If a user is logged in, the user_id field is used to identify the user's id as well. The user_rank field is a bit redundant, but that's because I wanted to have information about a user's privileges through the sessions table alone. In this case I've asigned each page a minimum rank required in order to view that page. This allows me to assign different user ranks required in order to view each page. This, of course is just my choice, and everyone can implement it according to their own needs.

There is also a lang field, which is used to store user's preferred language.


`sessions` table structure

CREATE TABLE `sessions` (
  `ses_id` INT(11) UNSIGNED NOT NULL,
  `start_time` INT(11) UNSIGNED NOT NULL,
  `last_time` INT(11) UNSIGNED NOT NULL,
  `user_ip` CHAR(16) NOT NULL,
  `lang` CHAR(2) NOT NULL DEFAULT 'en',
  `user_rank` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
  `user_agent` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`ses_id`)
);


`users` table structure (example)
CREATE TABLE `users` (
  `user_id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_name` VARCHAR(30) NOT NULL,
  `user_rank` TINYINT(2) NOT NULL DEFAULT '1',
  `user_pass` CHAR(32) NOT NULL,
  `user_last_time` INT(11) DEFAULT NULL,
  PRIMARY KEY  (`user_id`),
  KEY `user_name` (`user_name`)
);

The iSession class


class iSession
{
// configuration variables (private)

var $time2live = 3600;

// private class variables

var $sid, $session_info, $changed = false;

// class functions

}

I've named my class iSession, having some (private) variables. Since I'm using PHP 4 classes (for compatibility mainly) I will not worry about the variable scope and such, but I will try to comply with OOP principles.

The $time2live variable is for configuration, you can create a method for changing it in your script if you wish, it represents the number of seconds a session will "live" (the time until it is deleted).

The $sid represents the session in, it is assigned when the class instance is created.

The $session_info contains the actual values of the current session that are stored in the database. You can use a different approach and execute a SELECT each time you want to read a value and UPDATE when you wish to modify it, but from my experience it will result in lots of queries which CAN be avoided. So, we will get the data from the database when the instance is created and update it at the end of the script.

Since PHP 4 classes do NOT accept destructors, we will have to call a session_end() function manually. We will explain that more later on.

The $changed variable is a little tweaking I've created to see if during the current session there has been (manually) changed at least a variable. We know that at the end of each page load we have to change AT LEAST the last_time in the database in order to keep track of each variable's lifetime, but if no other variable was changed, we can shorten the update query. We will see more about this when we get to the update_session() function.

Functions used

Ok, let's see which functions we need.

First, we will make a constructor for the class. It will have to handle any session id transmitted through any GET, POST or COOKIE method. We just check the global $_REQUEST variable for this. Then, if the session id is valid (if it simply "looks" like a valid sid, we still need to make additional tests to see if there is a valid session associated with it), we try to create a new session with it.

Once the session is created, a "true" sid is returned which is our VALID sid for this session. We try to send it as a cookie and then read the session info from the table.

function iSession()
{
$in_sid = 0;

if (isset($_REQUEST['sid']) && $this->valid_sid($_REQUEST['sid']))
{
    $in_sid = $_REQUEST['sid'];
}

$this->sid = $this->session_start($in_sid);

$time2live = time() + $this->time2live;

setcookie ("sid", $this->sid, $time2live);

$this->session_info = $this->read_session();
}

The valid_sid() function checks only if the sid we got from the GET, POST or COOKIE method is indeed as it should, in our case an integer with the length between 1 and 11 - this way we also prevent and possible SQL injections or other problems that may be caused.

function valid_sid ($sid)
{
$sid = trim($sid);

if (empty($sid))
{
    return false;
}

if (!ereg("^[0-9]{1,11}$", $sid))
{
    return false;
}

return true;
}
The session_start() function is one of the most important functions since it is responsible of checking the sid we got from input and all additional checking in order to determine if we can reuse an existing session or we need to create a new one.

I've also used a little trick, if the user is NOT logged in, even if we didn't get any sid from the input, try to reuse an open session based on the IP and agent. Remember, this is only if the user is not logged in, in order to prevent any problems.

Through this function (and the others that use SQL queries), I've used a special function called query(). This should be customized for your own needs. In my case I've implemented an SQL interface with an additional class iSQL. I will not go into much detail about this, if you are using sessions that use a database connection, you most likely use it for more things as well, so you should customize it to integrate into your own system.

In my case, the query() function sends a SQL query to the database and gets the result back into an array, each row in the result as a row in an array and each field in the database, a field in the array. This was my personal choice and you can (and should) change it to meet your needs.

A cleanup_session() is also used in order to delete any expired sessions (this is called before the actual session creation in this case, but can be created as a cron job for example).

function session_start ($sid = 0)
{

$user_ip = $_SERVER['REMOTE_ADDR'];

$user_agent = substr($_SERVER['HTTP_USER_AGENT'],0,255);

$this->cleanup_sessions();

$sq = "SELECT user_ip, user_agent
       FROM `sessions`
       WHERE ses_id='"
.$sid."'
       LIMIT 0, 1"
;
$sesinfo = $this->query ($sq);

if (!empty($sesinfo[0]))
{
    if ($sesinfo[0]['user_ip']==$user_ip && $sesinfo[0]['user_agent']==$user_agent)
    {
        // the session exists and we have the same IP and agent so we accept the session id

        $this->sid = $sid;
        return $sid;
    }
}

// try to reuse existing session (only if user is not logged in!)

$sq = "SELECT ses_id
       FROM `sessions`
       WHERE user_ip='"
.$ip."' AND user_agent='".substr($_SERVER['HTTP_USER_AGENT'],
0, 255)."'
AND logged_in=0
       LIMIT 0, 1"
;
$sesinfo = $this->query ($sq);

if (isset($sesinfo[0]['id']))
{
    // everything ok, reuse the session

    $this->sid = $sesinfo[0]['ses_id'];
    return $sesinfo[0]['ses_id'];
}

// no match so we have to create a new session

$this->sid = $this->create_session();
return $this->sid;
}

The cleanup function:

function cleanup_sessions ()
{
$time2live = time() - $this->time2live;

$sq = "DELETE FROM `sessions`
       WHERE last_time<"
.$time2live;
$this->query($sq);
}

The query function simply calls a method of the global $sql variable.

function query ($sq)
{
global $sql;
return $sql->query($sq);
}
If we can't reuse an existing session, a create_session() function is called. This first creates a new sid (checking a session with the same sid doesn't already exist) and uses an INSERT query to add the new record to the database.

function create_session ()
{
$tm = time();

// generate a new session id

do
{
    $sid = $this->random_number(100000, 10000000-1);
} while ($this->session_exists($ses_id));

$sq = "INSERT INTO `sessions`
    (ses_id, start_time, last_time, user_ip, user_agent)
    VALUES
    ('"
.$sid."', '".$tm."', '".$tm."', '".$_SERVER['REMOTE_ADDR']."',
'"
.substr($_SERVER['HTTP_USER_AGENT'],0,255)."')";
$this->query($sq);

return $sid;
}

Some additional functions were used, first one checking if a session with that sid exists already in the database and the second generates a random number between $min and $max.

function session_exists ($sid)
{
$sq = "SELECT ses_id
    FROM `sessions`
    WHERE ses_id='"
.$sid."'
    LIMIT 0, 1"
;
$ses_exist = $this->query($sq);

if (!isset($ses_exist[0]['ses_id']))
{
    return false;
}
return true;
}
function random_number ($min, $max)
{
return mt_rand($min,$max);
}

Another important function is the session_end() function, which must be called at the end of the script (or if possible to be automated through a destructor). This function updates any changes made to the session variables to the database. For more flexibility, the session_end() function calls an update_session() function which can serve other purposes later on.

function session_end ()
{
$this->update_session();
}
The update_session() function updates each variable in the database (or only the last_time if no changes have been made).
function update_session ()
{
$param = "";

if ($this->changed)
{
    $param = "";
 
    reset ($this->session_info);
    while (list ($key, $val) = each ($this->session_info))
    {
        // skip the id field and last_time

        if ($key!="id" && $key!="last_time")
        {
            $param .= ", ".$key."='".$val."'";
        }
    }

}

$sq = "UPDATE `sessions`
       SET last_time='"
.time()."'".$param."
       WHERE ses_id='"
.$this->sid."'
       LIMIT 1"
;
$this->query($sq);
}

Since we get all the information from the database in the constructor, we have a read_session() function which basically grabs the entire row with the session information from the database with our sid.

function read_session ()
{
$sq = "SELECT *
       FROM `sessions`
       WHERE ses_id='"
.$this->sid."'
       LIMIT 0, 1"
;
$sesinfo = $this->query($sq);

return $sesinfo[0];
}
In order to read or write values of the session variables, we use the get_var() and mod_var() functions:

function get_var ($var)
{
return $this->session_info[$var];
}
function mod_var ($var, $val, $immediate = false)
{
$this->changed = true;
$this->session_info[$var] = $val;

if ($immediate)
{
    $this->update_session();
}
}
Notice the $immediate parameter for the mod_var() in order to override the "caching" method used with storing database variables locally and just update them at the end of the script, in which case we simply update them "on the fly".

Putting all together

Ok, now we have all functions we need in order to make it operational so let's see an example of how to use this:
<?php
require ("iSQL.php");
include ("iSession.php");

global $sql; $sql = new iSQL();

$session = new iSession();

$logged_in = $session->get_var("logged_in");
echo "Logged_in: ".$logged_in." ";

$session->mod_var("logged_in", 1);

$logged_in = $session->get_var("logged_in");
echo "Logged_in after mod_var(): ".$logged_in." ";

$session->session_end();
?>


You should see (if all is configured correctly):

Logged_in: 0
Logged_in after mod_var(): 1

But if you refresh the page:

Logged_in: 1
Logged_in after mod_var(): 1
Which means that the variable was successfully stored in the database (or you can increment a test value to see it more clearly).

All files are available for download here: iSession.zip (4KB).

Conclusion

You might wonder if all this is really that necesarry, and to be honest, I ask myself the same question as well. It's quite a bit of work to make it actually do basicly the same thing as the default session management system in PHP.

Also, since it uses a database connection, it's actually slower than it.

With a simple test I got about 0.0021 seconds for iSesssion and 0.0004 seconds for the standard sessions. This means that the standard PHP session functions are indeed at least 5 times faster than our class. If you add into account that the simple operation of connecting to the database takes a few milliseconds as well, you notice that it can be over 10 times slower to use the iSession class.

Still, we are talking about 1-2 milliseconds here, so the user will never see any difference (it takes at least 10 milliseconds to load a page for any user - usually a few seconds for a full page load). Also, if you are considering using database stored sesssion, you are most likely using database connection for other pages as well, so the time required to create a database connection is used anyway.

As a conclusion, I would say that it can be useful to use a custom session system for a larger project, especially if you are using OOP or some other type of code organization (perhaps even a CMS).

Related links:

Source code (4KB)
Creating a customized session handling system in PHP (part I)
PHP official website
MySQL official website

Posted by: Indy on August 4, 2006 at 11:54.
 

» Comments

SQL Injection vulnerability
<code>$sq = "SELECT ses_id
FROM `sessions`
WHERE user_ip='".$ip."' AND user_agent='".substr($_SERVER['HTTP_USER_AGENT'],
0, 255)."'
AND logged_in=0
LIMIT 0, 1";</code>

You use the $_SERVER['HTTP_USER_AGENT'] variable without sanitizing it. Using a forged User-Agent makes your code vulnerable to SQL Injection.

Posted by Ovidiu Indrei on May 31, 2009 at 09:06 AM.

Creating a customized session handling system in PHP (part II)
This is a very interesting code you've proived, I'm new in PHP and was interested to see how the user table get to work together with session table. The example provided is rather manual, so if you I could get to understand the purpose and function of users table I'd be a happy man.

Thanks

Posted by Kagg on September 4, 2009 at 09:24 AM.

Session hadling
Sir,
For power shut down also this code will work?

Posted by Sweta on August 11, 2010 at 05:26 AM.

topey
im a wonder boy

Posted by khan saifullah on March 1, 2011 at 01:34 PM.

I wonder
Hey everyone,

I wonder whether with this scrip, one will be able to automatically log out users who have been inactive for a certain amount of time.

I'm setting up a web application and I need to implement such sessions which would disactivate a user who have hasn't been active for like 15 minutes so that he will be required to sign in again in order to keep on working.

Please help!

Posted by Prince Kwamiso on February 25, 2012 at 08:19 AM.

Random Article


Search


Feeds


Bubble.ro RSS Feed

All Categories


Articles


Aetolia - The Midnight Age
How to create the histogram of an image using PHP
How to convert an image to grayscale using PHP
How to check if an image is grayscale in PHP
Interchanging 2 variables without the use of a third
Error launching browser window:no XBL binding for browser
Convert the AOL user session collection to a MySQL database
Introduction to Matlab
Creating a customized session handling system in PHP (part II)
Creating a customized session handling system in PHP (part I)
Firefox crashing with Yahoo! Messenger
ADL Search for oDC
Video codecs explained
Browsershots
How to use Auto-Away Message with oDC
Create complete Windows XP disk with SP2 and all updates
Data Execution Prevention error message in Windows XP
Google Mars
Logarithmic scale graphs in Excel
Urban Dictionary (or wtf does l33t mean?)
Learn more about BIOS
Backup your Firefox and Thunderbird settings
Syndicate your Yahoo 360 profile
What is Google PageRank?
'Cannot Open the File: Mk:@MSITStore' Error Message
Get your Gmail with Mozilla Thunderbird
E-Books links
Change the size of your Explorer thumbnails
Remove previews from Windows Explorer
How can I turn off system beeps?
How do I disable Internet Explorer?
What are proxies or how do I protect my anonymity?
How to set aliases triggers or macros in MushClient
What is RSS?
Palm Zire 31 fast review
oDC Installation and Basic Configuration
How I built a 2x80W amplifier (using power modules)
Leech/HotLink Protection
How to block referrer detection?
How to find out your IP address
Getting started with Mushclient
What is spyware and how do I protect my PC from it?
Stumble Upon - random surfing around the web
Automatic file backup for Windows users
How can I read foreign language sites?
Protect your web surfing privacy!
What is BitTorrent?
No more ads! Adblock for Firefox
Why use Firefox instead of Internet Explorer?
How do I create my own Yahoo ID?
© Copyright 2006-2020 Bubble. All rights reserved. Sitemap - Contact