Intranet Journal
The online resource for intranet professionals
Creating a PHP-Based Content Management System, Part 5
11/8/2004
|
|
| Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum |
Welcome to the penultimate installment of the series. So far we've looked at the basics of database interaction using PHP, as well as some vital techniques such as validation and error handling. We've allowed anyone to be able to add or remove content at the touch of a button, without programming knowledge. However, we've been rather to liberal in allowing 'anyone' to make changes. We need to keep certain areas of the site, such as the admin system, private. Sadly, few people will respect a "keep out" sign, and this month we'll be creating a class that'll act as a guard for the more private areas of your Intranet.
I should emphasize that this is a very basic type of security, and techniques such as secure servers, secure data transmission and encryption are not covered by this article. These should be investigated if you're planning on storing any sensitive information.
| Note: An updated copy of DbConnector.php, created earlier in the series, is required and is included with this article. There are two changes: blank lines after the closing ?> have been removed, and new functions have been created for extracting the last query used (helpful for debugging), and for returning the number of results found by a query (thanks to forum member w0lf for suggesting that). |
Let us now consider what we want our system to do:
The first bit we'll need to secure is the admin area, used for adding and removing content on the site. To get started, we'll set up database tables to store our user information.
| Table: Groups | |
| Column | Purpose |
| ID | ID of the group |
| groupname | Name of the group |
| Table: Users | |
| Column | Purpose |
| ID | ID of the user |
| user | A unique username for the user |
| pass | The user's password, encrypted |
| thegroup | Group to which the user belongs |
| firstname | The user's first name |
| surname | The user's surname |
| enabled | A 1 or a
0 specifies whether the user is enabled, allowing you to block troublesome ones |
SQL queries for creating the above two tables are below, which can be run in any SQL client:
# Create groups table
CREATE TABLE `cmsgroups` (
`ID` int(4) unsigned NOT NULL auto_increment,
`groupname` varchar(15) default NULL,
PRIMARY KEY (`ID`)
) TYPE=MyISAM;
# Create 10 groups, where 1 has the highest security
INSERT INTO `cmsgroups` VALUES (1,'Admin');
INSERT INTO `cmsgroups` VALUES (2,'Editors');
INSERT INTO `cmsgroups` VALUES (3,NULL);
INSERT INTO `cmsgroups` VALUES (4,NULL);
INSERT INTO `cmsgroups` VALUES (5,NULL);
INSERT INTO `cmsgroups` VALUES (6,NULL);
INSERT INTO `cmsgroups` VALUES (7,NULL);
INSERT INTO `cmsgroups` VALUES (8,NULL);
INSERT INTO `cmsgroups` VALUES (9,NULL);
INSERT INTO `cmsgroups` VALUES (10,'Anonymous');
# Create user table
CREATE TABLE `cmsusers` (
`ID` int(4) unsigned NOT NULL auto_increment,
`user` varchar(20) default NULL,
`pass` varchar(20) default NULL,
`thegroup` int(4) default '10',
`firstname` varchar(20) default NULL,
`surname` varchar(20) default NULL,
`enabled` int(1) default '1',
PRIMARY KEY (`ID`)
) TYPE=MyISAM;
# Create sample user
INSERT INTO `cmsusers` VALUES (1,'admin',PASSWORD('admin'),1,'Mr','Admin',1);
So how should we go about securing our site? We're going to write a class called Sentry to check whether a user is logged in. The system we're going to rely on is called sessions, a method of storing a user's details for the duration of their visit to the website or Intranet.
The constructor function (the function executed when the class is created) is as follows:
function sentry(){
session_start();
header("Cache-control: private");
}
This simply tells PHP to start the session, and adds a header that stops the password being stored in the user's cache. The function to logout is equally simple:
function logout(){
unset($this->userdata);
session_destroy();
exit();
}
This destroys the variable containing the user's details, the session data, and prevents further code from being executed. We next create a function to check whether the user is already logged in, and optionally to actually perform a login. The function has a number of parameters:
function checkLogin($user
= '',$pass = '',$group = 10,$goodRedirect = '',$badRedirect = ''){
...
}
We pass the username and password checkLogin, and if either of these are incorrect then the page should redirect to the address stored in $badRedirect. If $user and $pass are correct, we redirect to $goodRedirect. $group is used to specify the minimum group level that's allowed to access this resource, we'll specify that group level 10 has the least security privileges, group 1 has the most. If checkLogin finds that the user is already logged in, it should confirm that the original username and password provided are still valid.
Our first clause in checkLogin takes a look at whether a user seems to be logged in, by checking whether the session variables already exist:
// User is already logged in, check credentials
if ($_SESSION['user'] && $_SESSION['pass']){
// Validate session data
...// Look up the user in the database by performing and SQL query
$getUser = $loginConnector->query("SELECT * FROM cmsusers WHERE user = '".$_SESSION['user']."' AND pass = '".$_SESSION['pass']."' AND thegroup <= ".$group.' AND enabled = 1');// Redirect to goodRedirect or badRedirect appropriately
...
Notice in the above code, we use the PASSWORD function in the SQL query. For those of you not familiar with this, here's how it works. When we originally create the user's record in the database, we don't store the plain password; instead we use the PASSWORD function to encrypt it. What is now stored in the database is an apparently random string of letters and numbers, and the original password can never be recovered (hopefully). When we then come to check a login, we perform the same jumbling function on the provided password, and compare the result with the string of letters stored previously. If they're the same, then the original passwords match, and the user is authenticated.
The next piece of code is used when a user hasn't previously logged in:
}else{
// Validate the input
...
// Lookup the user in the DB
$getUser = $loginConnector->query("SELECT * FROM cmsusers WHERE user = '$user' AND pass = PASSWORD('$pass') AND thegroup <= $group AND enabled = 1");
$this->userdata = $loginConnector->fetchArray($getUser);if ($loginConnector->getNumRows($getUser) > 0){
// Login OK, store session details
$_SESSION["user"] = $user;
$_SESSION["pass"] = $rowUser["pass"];
$_SESSION["group"] = $rowUser["thegroup"];
// Redirect if goodRedirect was provided
...} else {
// Login BAD, Destroy session data
unset($this->userdata);// Redirect to badRedirect if appropriate
return false;}
}
If more than one result is found in the database, i.e. the user's details were correct, the username, password and group are stored in the session. OK, We're pretty much done. Let's now test our class by creating a login form and a test page we want to restrict.
| Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum |
Creating the Login Form
We're going to create a form to allow the user to login using the Sentry class we created on the previous page. You can create this using any HTML editor, I include a simple one in the source files at the end. We want it to look something like:
| Login |
Make sure the form's action is set to the filename of the page containing it (e.g., login.php), and the method is set to post. The PHP we put on this page is simple:
<?php
require_once("../includes/Sentry.php");
$sentry = new Sentry(); // Create a sentry object
// Check the user's submitted login
is valid
if ($HTTP_POST_VARS['user'] != ''){
$sentry->checkLogin($HTTP_POST_VARS['user'],$HTTP_POST_VARS['pass'],10,'welcome.php','failed.php');
}
// Log out the user
if ($HTTP_GET_VARS['action']
== 'logout'){
$sentry->logout();
}
?>
And that should all work nicely. The final step is to secure a page. For the purposes of the demonstration we'll create a page called welcome.php, that just says "welcome to the admin area." We only want people in groups 1 and 2 (admin and editors) to be able to access it, so at the start of the file we put:
<?php
require_once('../includes/Sentry.php');
$theSentry = new Sentry();
if (!$theSentry->checkLogin(2)
){ header("Location:
login.php"); die(); }
?>
And hey presto, your page is secure. Make sure you include that code on every page you want to protect.
Once you have these basics in place, creating a fully fledged user management system isn't far away. You can create pages to allow for the automatic signup, editing, deleting and emailing of your members. Samples of all of these functions will be included in the final part of the series, which you can find here next month.
Until next time!