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]
posted @ 2012-02-09 12:15  有梦就能实现  阅读(676)  评论(0)    收藏  举报