Creating an Authentication Class

Creating an Authentication Class

In the final part of this chapter you'll learn how to adapt PHP's session handling technology to talk to a PostgreSQL database, which will be used to store the session data hitherto stored on disk. At the same time, you'll put together a class to handle user authentication that you should be able to reuse on any project.

Connecting PHP Session Management to a Database

Getting PHP to use a database as opposed to the server's disk to store its data is far easier than you might expect. The following examples use PostgreSQL, but this method can easily be adapted to work with MySQL, XML flat files, or whatever you prefer.

The key to all this is just one PHP function: session_set_save_handler. If you take a look at the PHP manual reference entry for the function, you can see its syntax quite clearly:

   bool session_set_save_handler ( string open, string close, string    read, string write, string destroy, string gc) 

The idea is simple. You call this function before session_start() is used on any page that employs sessions. The function instructs PHP which custom functions to call when certain session behavior takes place, such as when a session is started, finished, read to, written from, or destroyed. There are certain requirements regarding the parameters passed into these methods as well as the value they must return. These are outlined in some detail on the PHP manual page for session_set_save_handler, but the following sections cover the basics.

The open() Function

The open() function is called whenever session_start() is called. PHP passes two values, the path in which it thinks the session should be stored if it is to be saved on disk (which we can ignore for our purposes) and the cookie name (for example, PHPSESSID) it is using for the session. It needs to return a true value for the session to be regarded by PHP as having been successfully created.

The close() Function

The close() function (not to be confused with the destroy() function) is called at what is effectively the end of the execution of any PHP script that uses session handling. For your purposes, it need do nothing other than return a true value although it might be nice to close its database connection if it isn't going to be used any more. In most production environments, you'll be using a globally accessible handle to a database that's already open for other purposes, so you'll no doubt have code in place to close it down again at the end of the script anyway. In practice, therefore, all this function needs to return is a true value.

The read() Function

The read() function is used whenever an attempt to retrieve a variable from the $_SESSION hash is made. It takes the session identifier as its sole operand and expects a serialized representation of $_SESSION in its entirety to be returned. You won't actually be using this in your class because you'll be doing your own session variable handling.

The write() Function

The write() function is used whenever an attempt to change or add to $_SESSION is made. It takes the session identifier, followed by the preserialized representation of $_SESSION, as its two parameters. It expects true to be returned if the data is successfully committed. This method is called even if no session variables are registered, and it is the first time the generated session ID is revealed to you.

The destroy() function

The destroy() function is called whenever the session_destroy function is used in code. It must return true upon execution.

The gc() Function

The gc() (garbage cleanup) function should be able to accept the "maximum lifetime of session cookies'' parameter as its only operand and get rid of any sessions older than that lifetime. It should return true when it's done. This function appears to be called just before open() so that PHP rids itself of any expired sessions before they may be used.

Introducing the UserSession Class

The UserSession class is a convenient way of implementing an object-oriented approach to session management as well as providing basic authentication methods for your applications. Here, you will implement your own methods to replace those of PHP, using the session_set_save_handler() method discussed previously.

It will be an entirely self-contained class that hides all PHP's session_ functions from your application's main body.

It also provides session variable handling, which bypasses PHP's own. Rather than store multiple variables in a single serialized hash, your methodology will use separate table rows for each variable. This could speed up access immensely. Note, however, that the previous session handling instruction method was not designed to cope with class methods, so you'll have to be rather cunning in your implementation.

Database Schema

The class depends upon three tables existing. The SQL (PostgreSQL flavor) to recreate these tables is in the following code. Create a new database with these tables in them before you go any further.

You can customize the user table to suit your needs. You will probably want to store more than just first and last name. Note that we've included a column called last_impression, too. This is used to store the time and date at which the user last made an impression (that is, requested a page) against his or her session. This is used to calculate session timeouts.

   CREATE TABLE user_session (      "id" SERIAL PRIMARY KEY NOT NULL,      "ascii_session_id" character varying(32),      "logged_in" bool,      "user_id" int4,      "last_impression" timestamp,      "created" timestamp,      "user_agent" character varying(256)    );    CREATE TABLE "user" (      "id" SERIAL PRIMARY KEY NOT NULL,      "username" character varying(32),      "md5_pw" character varying(32),      "first_name" character varying(64),      "last_name" character varying(64)    );    CREATE TABLE "session_variable" (      "id" SERIAL PRIMARY KEY NOT NULL,      "session_id" int4,      "variable_name" character varying(64),      "variable_value" text    ); 

As you can see, the sessions are stored as indexed by a standard serial ID rather than by their PHP-generated session ID. This allows for far faster indexing when you look at session variables (numbers always index better than strings).

It's worth creating a test user at this stage (for example, username 'ed', password '12345'). You need to know the MD5 representation of this password to enter it into the database. Of course, in the real world you'd have an application to do this, but for now, here's the SQL you need to make the examples that follow work:

   INSERT INTO "user"(username,md5_pw,first_name,last_name) VALUES    ('ed','827ccb0eea8a706c4c34a16891f84e7b', 'Ed', 'Lecky-Thompson'); 

First, take a look at the complete code for the class usersession.phpm. Don't worry. You'll go through it all, including how to tell session_set_save_handler to use class methods, right after you've tried it.

The Code: usersession.phpm

Remember that you use the .phpm extension to explicitly signify this as a class rather than as a template or an executable script in its own right.

   <?php      class UserSession {        private $php_session_id;        private $native_session_id;        private $dbhandle;        private $logged_in;        private $user_id;        private $session_timeout = 600;      # 10 minute inactivity timeout        private $session_lifespan = 3600;    # 1 hour session duration        public function __construct() {          # Connect to database          $this->dbhandle = pg_connect("host=db dbname=prophp5 user=ed") or die    ("PostgreSQL error: --> " . pg_last_error($this->dbhandle));          # Set up the handler          session_set_save_handler(              array(&$this, '_session_open_method'),              array(&$this, '_session_close_method'),              array(&$this, '_session_read_method'),              array(&$this, '_session_write_method'),              array(&$this, '_session_destroy_method'),              array(&$this, '_session_gc_method')          );          # Check the cookie passed - if one is - if it looks wrong we'll scrub    it right away          $strUserAgent = $GLOBALS["HTTP_USER_AGENT"];          if ($_COOKIE["PHPSESSID"]) {           # Security and age check           $this->php_session_id = $_COOKIE["PHPSESSID"];           $stmt = "select id from \"user_session\" where ascii_session_id = '" .    $this->php_session_id . "' AND ((now() - created) < ' " . $this->session_    lifespan . " seconds') AND user_agent='" . $strUserAgent . "' AND ((now() -    last_impression) <= '".$this->session_timeout." seconds' OR last_impression    IS NULL)";           $result = pg_query($stmt);           if (pg_num_rows($result)==0) {             # Set failed flag              $failed = 1;             # Delete from database - we do garbage cleanup at the same time             $result = pg_query("DELETE FROM \"user_session\" WHERE    (ascii_session_id = '". $this->php_session_id . "') OR (now() - created)    > $maxlifetime)");             # Clean up stray session variables             $result = pg_query("DELETE FROM \"session_variable\" WHERE    session_id NOT IN (SELECT id FROM \"user_session\")");             # Get rid of this one... this will force PHP to give us another             unset($_COOKIE["PHPSESSID"]);           };          };          # Set the life time for the cookie          session_set_cookie_params($this->session_lifespan);          # Call the session_start method to get things started          session_start();        }        public function Impress() {          if ($this->native_session_id) {            $result = pg_query("UPDATE \"user_session\" SET last_impression =    now() WHERE BACKGROUND-COLOR: #d9d9d9">};        }        public function IsLoggedIn() {          return($this->logged_in);        }        public function GetUserID() {          if ($this->logged_in) {            return($this->user_id);          } else {            return(false);          };        }        public function GetUserObject() {          if ($this->logged_in) {            if (class_exists("user")) {              $objUser = new User($this->user_id);              return($objUser);            } else {              return(false);            };          };        }        public function GetSessionIdentifier() {          return($this->php_session_id);        }        public function Login($strUsername, $strPlainPassword) {          $strMD5Password = md5($strPlainPassword);          $stmt = "select id FROM \"user\" WHERE username = '$strUsername'    AND md5_pw = '$strMD5Password'";          $result = pg_query($stmt);          if (pg_num_rows($result)>0) {            $row = pg_fetch_array($result);            $this->user_id = $row["id"];            $this->logged_in = true;            $result = pg_query("UPDATE \"user_session\" SET logged_in = true,    user_id = " . $this->user_id . " WHERE id = " . $this->native_session_id);            return(true);          } else {            return(false);          };        }        public function LogOut() {          if ($this->logged_in == true) {            $result = pg_query("UPDATE \"user_session\" SET logged_in = false,    user_id = 0 WHERE id = " . $this->native_session_id);            $this->logged_in = false;            $this->user_id = 0;            return(true);          } else {            return(false);          };        }        public function __get($nm) {          $result = pg_query("SELECT variable_value FROM session_variable WHERE    session_id = " . $this->native_session_id . " AND variable_name = '" .    $nm . "'");          if (pg_num_rows($result)>0) {            $row = pg_fetch_array($result);            return(unserialize($row["variable_value"]));          } else {            return(false);          };        }        public function __set($nm, $val) {          $strSer = serialize($val);          $stmt = "INSERT INTO session_variable(session_id, variable_name,    variable_value) VALUES(" . $this->native_session_id . ", '$nm', '$strSer')";          $result = pg_query($stmt);        }        private function _session_open_method($save_path, $session_name) {          # Do nothing          return(true);        }        private function _session_close_method() {          pg_close($this->dbhandle);          return(true);        }        private function _session_read_method($id) {          # We use this to determine whether or not our session actually exists.          $strUserAgent = $GLOBALS["HTTP_USER_AGENT"];          $this->php_session_id = $id;          # Set failed flag to 1 for now          $failed = 1;          # See if this exists in the database or not.          $result = pg_query("select id, logged_in, user_id from \"user_session\"    where ascii_session_id = '$id'");          if (pg_num_rows($result)>0) {           $row = pg_fetch_array($result);           $this->native_session_id = $row["id"];           if ($row["logged_in"]=="t") {             $this->logged_in = true;             $this->user_id = $row["user_id"];           } else {             $this->logged_in = false;           };          } else {            $this->logged_in = false;            # We need to create an entry in the database            $result = pg_query("INSERT INTO user_session(ascii_session_id,    logged_in, user_id, created, user_agent) VALUES ('$id','f',0,now(),    '$ strUserAgent')");            # Now get the true ID            $result = pg_query("select id from \"user_session\" where    ascii_session_id = '$id'");            $row = pg_fetch_array($result);            $this->native_session_id = $row["id"];          };          # Just return empty string          return("");        }        private function _session_write_method($id, $sess_data) {          return(true);        }        private function _session_destroy_method($id) {          $result = pg_query("DELETE FROM \"user_session\" WHERE ascii_session_id =    '$id'");          return($result);        }        private function _session_gc_method($maxlifetime) {          return(true);        }      }    ?> 

The Code: Testing the UserSession Class

Before going through the code, test the class for an imaginary user logging in and out. The following simple script shows the class in action:

   <?php    require_once("usersession.phpm");    $objSession = new UserSession();    $objSession->Impress();    ?>    UserSession Test Page    <HR>    <B>Current Session ID: </B> <?=$objSession->GetSessionIdentifier();?><BR>    <B>Logged in? </B> <?=(($objSession->IsLoggedIn() == true) ? "Yes" : "No")?><BR>    <BR><BR>    Attempting to log in ...    <?php $objSession->Login("ed","12345"); ?>    <BR><BR>    <B>Logged in? </B> <?=(($objSession->IsLoggedIn() == true) ? "Yes" : "No")?><BR>    <B>User ID of logged in user: </B> <?=$objSession->GetUserID();?><BR>    <BR><BR>    Now logging out...    <?php $objSession->Logout(); ?>    <BR><BR>    <B>Logged in? </B> <?=(($objSession->IsLoggedIn() == true) ? "Yes" : "No")?><BR>    <BR><BR> 

Run this and you should see output similar to Figure 15-5.


Figure 15-5

If you click Refresh several times, you should see that you get the same output time and time again. The session identifier is perpetuated.

If you want to prove to yourself that the logged-in state of the session really is maintained, try commenting out the Logout line and clicking Refresh. You should see that the Logged in statement at the top of the page repeatedly returns Yes.

It's worth looking at the database, too. As soon as the user is logged in successfully, that flag is set against the session in the database table and remains until either the session itself is deleted or the explicit LogOut method is called.

   prophp5=# SELECT * FROM user_session ;     id  |      ascii_session_id      | logged_in | user_id |      last_impression         |          created           |                              user_agent    -----+----------------------------+-----------+---------+---------------------    -----+----------------------------+-------------------------------------------    --------------------------     168 | 51bd591g054sn3bp2dsur4pme3 | f         |       0 |2004-02-23 07:31:04.33    4694 | 2004-02-23 06:54:31.802746 | Mozilla/4.0 (compatible; MSIE 6.0;    Windows NT 5.1; .NET CLR 1.1.4322)    (1 row) 

Now try the session variable functionality:

   <?php    require_once("usersession.phpm");    $objSession = new UserSession();    $objSession->Impress();    ?>    UserSession Variable Test Page    <HR>    <B>Current Session ID: </B> <?=$objSession->GetSessionIdentifier();?><BR>    <B>Logged in? </B> <?=(($objSession->IsLoggedIn() == true) ? "Yes" : "No")?><BR>    <BR><BR>    <B>Current value of TESTVAR:</B> [<?=$objSession->TESTVAR?>]<BR>    <BR><BR>    Setting TESTVAR to 'foo'    <BR><BR>    <?php      $objSession->TESTVAR = 'foo';    ?>    <B>Current value of TESTVAR:</B> [<?=$objSession->TESTVAR?>]<BR>    <BR><BR> 

Run this just once and you will see something like Figure 15-6.


Figure 15-6

Click Refresh and you will see that the value of TESTVAR is perpetuated. The top current value statement will read foo as well. A quick glance at the session_variable table in the database shows what actually gets stored:

   prophp5=# SELECT * FROM session_variable ;     id | session_id | variable_name | variable_value    ----+------------+---------------+----------------      6 |        168 | TESTVAR       | s:3:"foo";    (1 row) 

As you can see, using serialization to store the data follows PHP. However, unlike PHP, we use a new row for each variable. This allows more than one variable to be stored, separately serialized, against an individual session. With PHP's native session variable handling, all variables are lumped together in a single associative array that is then serialized. This can have very serious performance implications; serialization and deserialization have a fair amount of overhead, and PHP is not the quickest language in the world when it comes to string processing that honor probably goes to PERL! With this in mind, the decision is duly made to go against the grain on this occasion.

Clearly, the UserSession class is incredibly easy to use. But take a look at the class itself in more detail, both to understand the logic behind it and be fully equipped to deploy it in production applications.

How it Works: The UserSession Class

Start by looking at the private member variables of the class.

Variable

Purpose

php_session_id

The 32 character PHP-generated session ID.

native_session_id

The native session ID used to identify the session in the database. Never sent to the Web browser. Used only for the purpose of database entity relationships.

Dbhandle

A database connection handle. In production environments this would be declared elsewhere as a global resource, and the UserS-ession class would make use of it the same as any other.

Logged_in

Is this session a logged-in session? If this is true, then a user ID will be available.

user_id

The ID (from the database table) of the currently logged-in user.

session_timeout

An inactivity timeout. If a period of time greater than this elapses between reported impressions, the session is destroyed.

session_lifespan

The maximum age of the session. This is given to PHP for the purpose of setting the cookie and is also used in garbage cleaning SQL to keep the database clean of dead sessions.

The Constructor

You want your class to be plug and play. This is so that the programmer can instantiate it once and forget about it.

The constructor:

Note the interesting syntax used for session_set_save_handler:

   session_set_save_handler(      array(&$this, '_session_open_method'),      array(&$this, '_session_close_method'),      array(&$this, '_session_read_method'),      array(&$this, '_session_write_method'),      array(&$this, '_session_destroy_method'),      array(&$this, '_session_gc_method')    ); 

Largely undocumented, parameters for this method do not necessarily have to be strings representing procedural function names. Instead you may pass an array of two components. The first is a reference to an instance of a class (in this case, &$this refers to this instance of the class). The second is the name of the method of that class.

The _session_read_method() Handler

Because the first method called by PHP's own session management after a valid session ID has been decided upon (through generation of a brand new session or the presentation of a still-valid cookie by the web browser), you use this method to ensure that the session database is kept fully up-to-date.

If the 32-character session identifier supplied by PHP does exist in the database, then the class member variables (logged_in and user_id) are updated against the database record. If it does not, it is inserted with defaults in place.

The Impress() Function

This method touches the session to indicate that a new page impression has taken place. Generally, this method would be called on any page that uses the session class directly after it has been instantiated.

The last impression column in the database is used for determining session timeouts, and so it is very important to call this method if you want a given page to count against a user's accrued inactivity timeout.

The IsLoggedIn() Function

This method simply reports from the private member variable as to whether this session has undergone a successful login in its lifespan that has not been rescinded by means of a logout().

The GetUserID() and GetUserObject() Functions

If a user is logged in, these two functions return, respectively, the ID of the logged in user (from the user table defined in the database) and, if possible, an instantiated object of the user class should one exist. One has not been defined in this chapter because it is slightly outside the scope of session management, but you will almost certainly wish to develop one for any serious production application because you will almost certainly frequently wish to read and write the properties of your currently logged-in user.

The class is instantiated using the ID as the sole operand of the constructor and works well with the discussion of GenericObject in Chapter 7, "GenericObject Class.''

The GetSessionIdentifier() Function

This method returns the 32-character PHP session identifier rather than the internal (database) identifier. This is the property more likely to be used in applications because the internal identifier is only really meaningful to postgreSQL, and is never exposed to the application or Web browser.

The _get() and _set() Overload Methods

Taking advantage of PHP5's support for overloading the inspection and assignment of ostensibly public member variables, the overload methods provide their own session variable functionality. Quite simply, a variable for a given session is set by simply assigning it a value. Likewise, it is read by simply reading it, as if it were a publicly declared member variable.

Putting it all Together

The UserSession class built in this chapter is a fully reusable, modular component. It easily can be included in virtually any project that has the requirement to maintain session data as well as support the authentication of registered users.

We have provided an object-oriented interface to PHP's own session management and replaced its session variable functionality with a more flexible variant whereby each variable is stored separately for the sake of speed and cleaner code.

Integration with your own applications should be most straightforward. You may find you need to extend its functionality to suit your own needs, but its being a self-contained class makes this significantly easier than extending PHP's session management out of the box.

It's also worth pointing out that the only additional security mechanism incorporated from earlier in the chapter is the User Agent check. Should you require firmer security, you may wish to explore integrating some of the other security functionality such as the IP variance analysis discussed earlier.



Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 182
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net