PHP框架设计入门之二:管理用户

 这是PHP应用程序框架设计系列教程的第二部分。在第一部分,我们已经介绍框架的基础类结构,并展示了项目的大体。这一部分,我们将在程序中添加会话处理功能,并演示管理用户的各种方法。

  会话

  HTTP是一种无状态的协议,正因为如此,它没有包含任何与服务器连 接的相关信息。这就意味着,HTTP是孤立的,web服务器并不知道用户与你web程序相连接的任何信息,并且服务器会将每个页面请求视为一个新的连接。 Apache/PHP通过提供对会话的支持来避开这一限制。从概念上来说,会话是相当简单的。在一个用户第一次连接到服务器的时候,他被分配一个唯一的 ID。web服务器在一个文件中维护会话信息(译注:即把会话信息存储到文件中),于是可以通过这个ID来定位用户信息。用户同样会在每次连接中维护这个 ID。最典型的作法,就是将ID存储在cookie中,之后,这个ID会作为请求-应答序列的一部分发回给服务器。如果用户不允许使用cookie,会话 ID同样可以在请求每个页面时,通过query字符串(即URL中?以后的部分)发回给服务器。因为web客户端会断开连接,所以web服务器会在一定周 期后,使那些不活动的会话信息过期。

  我们不想在这篇文章中过多地谈论Apache/PHP的配置,除了利用会话来维护用户信息。我们假 设会话支持功能已经开启,并在你的服务器上配置好了。我们将直接从本序列教程第一部分谈论系统基础类时,被我们搁在一边的地方谈起。你可能还记得 class_system.php的第一行是session_start(),这一句的作用是,如果不存在会话信息,则开始一个新的用户会话,否则不做其 他的事情。根据你服务器的配置,开始会话的时候,会话ID会被保存在客户端的cookie里或者作为URL的一部分进行传递。当你调用内建的 session_id()函数时,总可以得到会话ID。通过这些工具,我们现在可以建立一个web应用程序,它可以对用户进行验证,并且能够在用户浏览网 站不同页面的时候去维护用户的信息。如果没有会话,那么用户每一次请求页面的时候,我们就不得不提醒用户进行登录。

  那么,我们应该在会 话中存储什么信息呢?我们一下子就可以想到如用户名这类信息。如果你看一下class_user.php,你会看到其他要存储的数据。(在程序 中)include这个文件的时候,首先会检查用户是否登录(如果没有用户id,那么会设置一个默认的会话值)。注意,session_start()必 须在我们使用$_SESSION数组之前调用,$_SESSION数组包含所有我们的会话数据。UserID用来标识存储在我们数据库中的用户(如果您已 经完成了本系列教程的第一部分,那么这个数据库中的数据应该可以访问了)。Role(角色)是用来检测用户是否有足够的权限去访问程序中的某一部分功能。 LoggedIn标识用来检测用户是否通过验证,Persistent标识用来检测用户是否想依靠他们的cookie内容自动进行登录。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PHP]       //session has not been established
   if (!isset( $_SESSION [ 'UserID' ]) ) {
           set_session_defaults();
   }
 
   //reset session values
   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
   }[/PHP]

  用户数据

  我们将所有的用户数据存储到数据库的tblUsers表,这个表可以使用下面的SQL语句来创建(仅限MySQL)

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `tblUsers` (
  `UserID` int(10) unsigned NOT NULL auto_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=MyISAM COMMENT= 'Registered Users' ;

  这个语句建立了一个大概的用户表。大多数字段不言自明。我们用UserID这个字段来唯一标识每个用户。Login字段同样也必须是唯一的,存 储用户使用的登录名。Password字段用来存储用户密码的MD5散列值。我们没有存储实际的密码是因为安全和隐私的原因。我们可以拿用户输入的密码的 MD5散列值与数据表中的进行对比来验证用户。用户角色用来将用户分配到一个许可组。最后,我们用LastLogon, SessionID和SessionIP字段来跟踪用户对系统的使用情况,包括用户最后登录时间,用户最后使用的会话ID,用户机器的IP地址。用户每次 成功登录后,会调用user系统类中的_updateRecord()函数来更新这些字段值。这些字段同时也可以用来保证安全性,保证不受XSS(跨站脚 本)攻击。

 
1
2
3
4
5
6
7
8
9
10
11
12
[PHP] //Update session data 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 );
}[/PHP]

  安全问题

  这一部分看起来应该来考虑几个在开发web应用程序会遇到的安全问题。因为安全性是用户管理的一个主要方面,我们得非常细心,不在我们这一部分的代码中留下任何因为粗心导致的bug。

   第一个要考虑的问题是,不管在任何web应用程序中都会遇到的——SQL注入攻击(SQL注入会发送web数据来进行数据库查询)。在我们的情况中,我 们使用用户提供的登录名和密码来查询数据库进而验证用户。一个怀有恶意的用户可以提交SQL代码作为输入文本的一部分,从而可能达到下面的几个目的:1 不需要拥有有效的账号即可登录 2 探测我们数据库的内部结构 3 修改我们的数据库。下面是一个非常简单的例子,用来测试用户是否有效。

 
1
2
$sql = "SELECT * FROM tblUsers
        WHERE Login = '$username' AND Password = md5( '$password' )";

   设想一下,用户输入 admin'-- ,然后将密码框留空。服务器执行的SQL代码则为:SELECT * FROM tblUsers WHERE Login = 'admin'--' AND Password = md5('')。你是否发现问题了?代码不同时检查登录名和密码了,只是检查登录名,(因为)余下的部分被注释掉了。只要在表里面有一个admin用户, 这个查询就会返回一个肯定的回答。

  你该怎么样保护你自己的代码免受这种类型的威胁呢。第一步是检验任何从不可靠的来源(比如:用户)发 送到SQL服务器的数据。PEAR DB中的quote()函数为我们提供了这样的保护,这个函数可用于发送到SQL服务器的任何字符串。我们的login()函数(译注:该函数请见下文) 显示了我们可以采取的其他预防措施。在我们的代码中,我们在SQL服务器和PHP中(根据SQL服务器返回的记录)都检查了密码。这样的话,攻击必须同时 对SQL服务器和PHP都有效,才能使一个未验证的用户登录进去。你想说这杀伤力太大了吧?是的,也许吧。

  另一个问题是,我们必须警惕会话窃取和跨站脚本攻击(XSS)的可能性。我不想过多地谈论一个黑客冒 充其他已验证用户会话信息的各种方法,但确定的是那确实有可能。事实上,比起利用代码中的bug,许多基于社会工程学的方法更可以称得上是十分难解决的问 题。为了保护我们的用户不受这样的威胁,我们在用户每次登录的时候存储他的会话IP和会话ID。然后,当页面加载完成,我们就拿用户当前的会话ID和IP 地址和数据库中的值进行比对。如果不匹配,那么就破坏会话信息。这样子,如果一个黑客让一个受害者从一台机器上登录,然后试着从他自己的机器使用受害者的 活动会话,那么在他做出任何破坏之前会话就会被关闭。具体的实现代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[PHP] //check if the current 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();
    }
}[/PHP]

验证

  现在我们已经了解了各种相关的安全问题,下面我们来看一看验证用户的代码。login()函数接收一个登录名和密码,返回一 个Boolean(布尔值)来标明是否正确。正如上面所说的,我们必须假定传入函数中的值是来自于不可靠的来源,用quote()函数来避免问题。完整的 代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[PHP] //Login a user with name 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 sql injection)
    if ( $result and $result [ 'Password' ] == $md5pw ) {
         $this ->_setSession( $result );
         $this ->_updateRecord();      //update session info in db
         return true;
    } else {
        set_session_defaults();
         return false;
    }
}[/PHP]

  用户注销的时候,我们要清理在服务器上的会话变量,还有在客户端的会话cookie。我们还要关闭会话。代码如下:

 
1
2
3
4
5
6
[PHP] //Logout the current user (reset session)
function logout() {
    $_SESSION = array ();                //clear session
    unset( $_COOKIE [session_name()]);    //clear cookie
    session_destroy();                   //kill the session
}[/PHP]

  在每一个页面都要求验证,我们可以简单地检查一下会话,看用户是否已经登录,或者我们可以检查用户角色,看用户是否有足够的权利。角色被定义为一个数字(译者注:即用数字来表明角色),更大的数字意味着更多的权利,下面的代码使用角色来检查用户是否有足够的权利。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[PHP] //check if user has enough permissions
//$role is the minimum level required for entry
//Returns Boolean
function checkPerm( $role ) {
    if ( $_SESSION [ 'LoggedIn' ]) {
         if ( $_SESSION [ 'Role' ]>= $role ) {
             return true;
        } else {
             return false;
        }
    } else {
         return false;
    }
}[/PHP]

  登录/注销的接口

   现在我们已经有一个处理会话和用户账号的框架了,我们需要一个接口,这个接口允许用户登录和注销。使用我们的框架,建立这样的一个接口应该是十分简单 的。下面我们就从比较简单的logout.php页面开始,这个页面用来注销用户。这个页面没有任何内容展现给用户,只是在注销用户以后,简单将用户重定 向到index页面。

 
1
2
3
4
5
6
7
8
9
10
11
12
define( 'NO_DB' , 1);
define( 'NO_PRINT' , 1);
include "include/class_system.php" ;
 
class Page extends SystemBase {
    function init() {
         $this ->user->logout();
         $this ->redirect( "index.php" );
    }
}
 
$p = new Page();

  首先,我们定义NO_DB和NO_PRINT常量来优化加载这个页面的时间(正如我们在本系列教程中第一部分描述的那样)。现在,我们要做的所有事情,就是使用user类来注销用户,并在页面初始化事件中重定向到另外的页面。

   这个login.php页面需要一个接口,我们使用系统的表单处理能力简化处理的实现过程。至于这个过程是如何运作的,我们将会在本系列教程的第三和第 四部分详细介绍。现在呢,我们所需要知道的全部事情,就是我们需要一个HTML表单,这个表单与应用程序的逻辑相连接。表单代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[PHP]<form action= "<?=$_SERVER['PHP_SELF']?>" method= "POST" name= "<?=$formname?>" >
<input type= "hidden" name= "__FORMSTATE" value= "<?=$_POST['__FORMSTATE']?>" >
 
<table>
   <tr>
     <td>Username:
     <td><input type= "text" name= "txtUser" value= "<?=$_POST['txtUser']?>" ></td>
   </tr>
   <tr>
     <td>Password:
     <td><input type= "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>[/PHP]

你可能感兴趣的:(php框架)