转载 PHP Application Framework Design: 2 – Managing Users
转自:http://www.lbsharp.com/wordpress/index.php/2005/10/13/php-application-framework-design-2-managing-users/This is part 2of a multi-part series on the design of a complete application frameworkwritten in PHP. In part 1, we covered the basic class structure of theframework and laid out the scope of the project. This part adds sessionhandling to our application and illustrates ways of managing users.
Sessions
HTTP is astateless protocol and, as such, does not maintain any information about theconnections made to the server. This means that, with HTTP alone, the webserver cannot know anything about the users connected to your web applicationand will treats each page request as a new connection. Apache/PHP gets aroundthis limitation by offering support for sessions. Conceptually sessions are afairly simple idea. The first time a web user connects to the server, he isassigned a unique ID. The web server maintains session information in a file,which can be located using this ID. The user also has to maintain this ID witheach connection to the server. This is typically done by storing the ID in acookie, which is sent back to the server as part of the typicalRequest-Response1 sequence. If the user does not allow cookies, the session ID can also besent to the server with each page request using the query string (the part ofthe URL after the ?). Because the web client is disconnected, the web serverwill expire sessions after predefined periods of inactivity.
We will not goover configuring Apache/PHP in this article but will utilize sessions tomaintain user information in our application. It is assumed that sessionsupport is already enabled and configured on your server. We will pick up wherewe left off in part 1 of this series when we described the system base class.You may recall that the first line in class_system.php is session_start(), which starts a new user session if noneexists or does nothing otherwise. Depending on how your server is configured,this will cause the session ID to be saved in the clients cookie file or passedas part of the URL. The session ID is always available to you by calling thebuild in function session_id(). With these tools at hand, we can nowbuild a web application that can authenticate a user and maintain the usersinformation as he is browsing the different pages on the site. Withoutsessions, we would have to prompt the user for their login credentials everysingle time they request page.
So what will wewant to store in the session? Lets start with the obvious like the users name.If you take a look at class_user.php you will see the rest of the data being stored. When this file isincluded, the first thing that is checked is whether a user is logged in(default session values are set if the users id is not set). Note that the session_start() must have already been called before we start playing with the $_SESSIONarray which contains all our session data. The UserID will be used to identify the user in our database (which should already beaccessible after part one of this series). The Role will be used to determine whether the user has sufficient privileges toaccess certain features of the application. The LoggedIn flag will be used to determine if the user has already been authenticatedand thePersistent flag will be used to determine whether the user wants to automatically belogged in based on their cookie content.
//session has notbeen established
if(!isset($_SESSION['UserID']) ) {
set_session_defaults();
}
//reset sessionvalues
function set_session_defaults(){
$_SESSION['UserID'] = '0'; //User ID in Database
$_SESSION['Login'] = ''; //Login Name
$_SESSION['UserName'] = ''; //User Name
$_SESSION['Role'] = '0'; //Role
$_SESSION['LoggedIn'] = false; //is user logged in
$_SESSION['Persistent'] = false; //is persistent cookie set
}
User Data
We store all theuser data in our database in table tblUsers. This table canbe created using the following SQL statement (mySQL only).
CREATE TABLE`tblUsers` (
`UserID` int(10) unsigned NOT NULLauto_increment,
`Login` varchar(50) NOT NULL default '',
`Password` varchar(32) NOT NULL default '',
`Role` int(10) unsigned NOT NULL default '1',
`Email` varchar(100) NOT NULL default '',
`RegisterDate` date default '0000-00-00',
`LastLogon` date default '0000-00-00',
`SessionID` varchar(32) default '',
`SessionIP` varchar(15) default '',
`FirstName` varchar(50) default NULL,
`LastName` varchar(50) default NULL,
PRIMARY KEY (`UserID`),
UNIQUE KEY `Email` (`Email`),
UNIQUE KEY `Login` (`Login`)
) TYPE=MyISAMCOMMENT='Registered Users';
This statementcreates a bare-bones user table. Most of the fields are self explanatory. Weneed the UserID field to uniquely identify each user. The Login field, which must also be unique, stores the user’s desired login name.The Password field stores the MD5 hash of the user’spassword. We are not storing the actual password for security and privacyreasons. Instead we can compare the MD5 hash of the password entered with thevalue stored in this table to authenticate the user. The user’s Role will be used to assign the user to a permission group. Finaly, we will usethe LastLogon, SessionID, and SessionIP fields to track the user’s usage of our system including the last time theuser logged in, the last PHP session ID the user had, and the IP address of theuser’s host. These fields are updated each time the user successfully logs inusing the _updateRecord() function in the user system class. These fields are also used for security in preventingcross-site scripting attacks.
//Update sessiondata on the server
function_updateRecord () {
$session = $this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "UPDATE tblUsers SET
LastLogon = CURRENT_DATE,
SessionID = $session,
SessionIP = $ip
WHERE UserID = $this->id";
$this->db->query($sql);
}
Security Issues
This seems like alogical place to address several security issues that come up when developingweb applications. Since security is a major aspect of user management, we needto be very careful not to leave any careless bugs in this part of our code.
The first issuethat needs to be addressed is the potential for SQL injection in any webapplication that uses posted web data to query a database. In our case, we usethe login name and password supplied by the user to query the database andauthenticate the user. A malicious user can submit SQL code as part of inputfield text and may potentially achieve any of the following: 1) login withouthaving a valid account, 2) determine the internal structure of our database or3) modify our database. The simplest example of this is the SQL code used totest if the user is valid.
$sql = "SELECT* FROM tblUsers
WHERE Login = '$username' AND Password= md5('$password')";
Suppose the userenters admin'-- and leaves the password blank. The SQL codeexecuted by the server is: SELECT * FROM tblUsers WHERE Login = 'admin'--' AND Password = md5(''). Do you see the problem? Instead of checking the login name and password,the code only the checks the login name and the rest is commented out. As longas there is a user admin in the table, the query will return apositive response. You can read about other SQL injection exploits in DavidLitchfield’spublication.
How do you protectyourself from this kind of threat. The first step is to validate any data sentto the SQL server that comes from an untrusted source (i.e. the user). PEAR DBprovides us with this protection using the quote() function which should be used on any string sent to the SQL server. Our login() function shows other precautions that we can take. In the code, weactually check the password in both the SQL server and in PHP based on therecord returned. This way, the exploit would have to
work for both the SQL server and PHP for an unauthorized user to get in.Overkill you say? Well, maybe.
Another issue thatwe have to be aware of is the potential for session stealing and cross sitescripting (XSS). I won’t get into the various ways that a hacker can assume thesession of another authenticated user but rest assured that it is possible. Infact, many methods are based on social engineering rather than bugs in theactual code so this can be a fairly difficult problem to solve. In order toprotect our users from this threat, we store the Session IP and Session ID ofthe user each time he logs in. Then, when any page is loaded, the users currentSession ID and IP address are compared to the values in the database. If thevalues don’t match then the session is destroyed. This way, if a hacker gets avictim to log in from one machine and then tries to use that active sessionfrom his own machine, the session will be closed before any harm can be done.The code to implement this is bellow.
//check if thecurrent session is valid (otherwise logout)
function _checkSession(){
$login = $this->db->quote($_SESSION['Login']);
$role = $this->db->quote($_SESSION['Role']);
$session =$this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "SELECT * FROM tblUsers WHERE
Login = $login AND
Role = $role AND
SessionID = $session AND
SessionIP = $ip";
$result = $this->db->getRow($sql);
if ($result) {
$this->_setSession($result);
} else {
$this->logout();
}
}
Authentication
Now that weunderstand the various security issues involved, lets look at the code forauthenticating a user. The login() function accepts a login name and passwordand returns a Boolean reply to indicate success. As stated above, we mustassume that the values passed into the function came from an untrusted sourceand use the quote() function to avoid problems. The complete codeis provided below.
//Login a user withname and pw.
//Returns Boolean
function login($username,$password) {
$md5pw = md5($password);
$username =$this->db->quote($username);
$password =$this->db->quote($password);
$sql = "SELECT * FROM tblUsers WHERE
Login = $username AND
Password = md5($password)";
$result = $this->db->getRow($sql);
//check if pw is correct again (prevent sqlinjection)
if ($result and $result['Password'] ==$md5pw) {
$this->_setSession($result);
$this->_updateRecord(); //update session info in db
return true;
} else {
set_session_defaults();
return false;
}
}
To logout, we haveto clear the session variables on the server as well as the session cookies onthe client. We also have to close the session. The code below does just that.
//Logout thecurrent user (reset session)
function logout() {
$_SESSION = array(); //clear session
unset($_COOKIE); //clear cookie
session_destroy(); //kill the session
}
In every page thatrequires authentication, we can simply check the session to see if they user islogged in or we can check the user’s role to see if the user has sufficientprivileges. The role is defined as a number with the larger numbers indicatingmore rights. The code below checks to see if the users has enough rights usingthe role.
//check if user hasenough permissions
//$role is theminimum level required for entry
//Returns Boolean
functioncheckPerm($role) {
if ($_SESSION['LoggedIn']) {
if ($_SESSION['Role']>=$role) {
return true;
} else {
return false;
}
} else {
return false;
}
}
Login/LogoutInterface
Now that we have aframework for handling sessions and user accounts, we need an interface toallow the user to login and out. Using our framework, creating this interfaceshould be fairly easy. Let us start with the simpler logout.php page which will be used to log a user out. This page has no content todisplay to the user and simply redirects the user to the index page afterhaving logged him out.
define('NO_DB', 1);
define('NO_PRINT',1);
include"include/class_system.php";
class Page extendsSystemBase {
function init() {
$this->user->logout();
$this->redirect("index.php");
}
}
$p = new Page();
First we definethe NO_DB and NO_PRINT constants to optimize the loading time of this page (as described in Part1 of this series). Now all we have to do is use theuser class to log the user out and redirect to another page in the page’sinitialization event.
The login.php page will need an interface and we will use the system’s form handlingabilities to simplify the implementation process. Details of how this workswill be described in Parts 3 and 4 of this series. For now, all we need to knowis that we need an HTML form that is linked the application logic. The form isprovided below.
<formaction="<?=$_SERVER['PHP_SELF']?>" method="POST"name="<?=$formname?>">
<input type="hidden"name="__FORMSTATE"value="<?=$_POST['__FORMSTATE']?>">
<table>
<tr>
<td>Username:</td>
<td><input type="text"name="txtUser"value="<?=$_POST['txtUser']?>"></td>
</tr>
<tr>
<td>Password:</td>
<td><inputtype="password" name="txtPW"value="<?=$_POST['txtPW']?>"></td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox"name="chkPersistant" <?=$persistant?>>
Remember me on this computer
</td>
</tr>
<tr style="text-align: center;color: red; font-weight: bold">
<td colspan="2">
<?=$error?>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit"name="Login" value="Login">
<input type="reset"name="Reset" value="Clear">
</td>
</tr>
</table>
</form>
Now we need thecode to log a user in. This code sample demonstrates how to use the systemframework to load the above form into a page template, handle the form events,and use the user class to authenticate the user.
class Page extendsSystemBase {
function init() {
$this->form = newFormTemplate("login.frm.php", "frmLogin");
$this->page->set("title","Login page");
if (!isset($_POST['txtUser'])&& $name=getCookie("persistantName")) {
$this->form->set("persistant","checked");
$_POST['txtUser']=$name;
}
}
function handleFormEvents() {
if (isset($_POST["Login"])) {
if($this->user->login($_POST['txtUser'],$_POST['txtPW'])){
if(isset($_POST['chkPersistant'])) {
sendCookie("persistantName", $_POST['txtUser']);
} else {
deleteCookie("persistantName");
}
$this->redirect($_SESSION['LastPage']);
} else
$this->form->set("error","Invalid Account");
}
}
}
$p = new Page();
On pageinitialization, the form is loaded into the page template, the page’s title isset and the user’s login name is pre-entered into the input field if thepersistent cookie is set. The real work happens when we handle the form events(i.e. when the user presses a button to submit the page). First we check if thelogin button was clicked. Then we use the login name and password submitted toauthenticate the user. If authentication is successful, we also set a cookie toremember the users name for the next time. If the authentication fails, anerror is displayed on the page.
Summary
So far, we havelaid the foundation for how our application will behave and how the frameworkwill be used. We added user management capabilities to our application andcovered several security issues. Read the next part to see how to implementpage templates and separate application logic from the presentation layer.
1. The Request-Response sequence is the keybehind many network protocols including HTTP. A user request is made by sendingthe server the URL of the desired page over a specific port (typically port 80for HTTP and 443 for HTTPS). The web server listens on the specified port andgenerates a response, which is composed of a header section and a body sectionseparated by two new line characters. The header section contains descriptiveinformation about the data being sent to the user including the contenttype/encoding and content length. The body section contains the desired data,which is typically the HTML page (of course it could be any file that the webserver is configured to send). When using server-side programming languagessuch as PHP, the web server has to pass the page request to the languagesinterpreter, which will run the page on the server and generate the HTMLoutput. For more information about the specification of the header and bodysections visit the World Wide Web Consortium.
页:
[1]