技术开发 频道

PHP 开发经典教程 (Part 4):会话管理

【IT168 专稿】耐心自付

既然你已经结合MySQL和SQLite使用PHP了,那么你可能会认为你已经知道了PHP编程所需的一切。实际上,你或许甚至一直在思考减少对Zend.com的访问,同时为了一些更炫和更酷的事情而放弃该系列教程的学习……

恩,大错特错。

你看,虽然内建的数据库支持使得使用PHP编程容易,但它并不是唯一的使得PHP如此受欢迎的原因。易于使用的XML API和新的异常处理机制(PHP 5中)、可插入模块的支持以及内建的session管理只是使得PHP震撼的许多其他特征中的一部分。而且如果你能自己发现所有这些能力并在那儿多逗留一会儿,那么PHP的所有的这些能力都将在这个系列教程里面被系统的深入研究。所以,请闭上你的眼睛,深呼吸然后继续阅读以发现本教程的所有主题:sessions和cookies。

聚会时间

也许你在你最近去过的派对上听说过这个:“HTTP是一个无状态协议,且互联网是一个无状态的开放环境”。

没有,恩,很明显,你没有去对地方。

简单来讲,上面的含义就是HTTP这种超文本传输协议是Web的中枢,它无法保持已连接到Web站点的每个客户的身份的记忆,因而将一个页面的每次请求作为唯一和独立的连接对待,而与之前的无论什么连接都没有关系。只要你漫无目的的在网上冲浪,那么这个“无状态环境”就会工作的很好,但它会对那些依赖之前请求所积累的数据的站点带来一个非常头痛的问题。最常见的例子就是一个在线购物车(在无状态环境下,当你从一个目录页面跳到另外一个时,明了你购物简单列表的所有条目会变得困难)。

那么,明显地,所需要的是一种使得“维护状态”变得可能的方法,该方法允许客户端连接被跟踪而且特定的连接数据得到维护。因而产生了cookies,cookies允许Web站点在客户端系统存储客户端的特定信息而且在无论何时需要的时候访问该信息。一个cookie只是一个文件,它包含一系列的变量-值对而且连接到某个域上。当客户端请求一个特定域时,该cookie文件中的值被读取然后导入到服务器环境中,在该环境中,开发者可以为不同的目的而读取、修改和使用这些值。Cookie是一种将数据从一次客户端访问带到下一次访问的简便方法。

另外一种常见的方法就是使用session来存储连接特定的数据;该session数据在访问期间被保存在服务器端,且在访问结束后就被销毁。Session通过为每个Session关联一个由PHP自动产生的SessionID号(该Session唯一的标识符)而工作。该SessionID被保存在两个位置中:在使用临时cookie的客户端上和在服务器上的普通文件或者数据库中。通过使用SessionID来给每个接受的请求一个名称,开发人员可以识别哪个客户发起了哪个请求,然后在Session变量(Session期间存活的而且可以存储文本或者数值信息的变量-值对)中跟踪和维护客户端特定的信息。

因此,Sessions和cookies提供了一种良好的方法来绕开HTTP协议的无状态特性,而且Sessions和cookies在许多当前最大的站点上被用于跟踪和维护个人或者商业交易的信息。通常情况下,你使用session存储在一个访问的过程中所需要的值,而cookie存储用于多次访问的更持久的数据。

PHP自PHP 3.0就已经包含了对cookies的支持,而且自PHP 4.0开始其内建了session管理。这两个特征都是默认激活的,因此你不需要做任何特别的事情来激活它们。相反,请向下翻动然后看看你的第一个session。

第一个Session

用于演示session如何工作的标准例子之一就是点击计数器应用。这是一个简单的计数器,它在你第一次访问Web页面时初始化一个变量,然后在你每次重载该页面的时候增加该变量的值。计数器变量存储于session中,这意味着如果你浏览其他站点然后返回,那么上次存储的计数器的值将会恢复(只要你不在中间期间通过关闭浏览器而销毁该session)。
看看下面的代码:
<?php // initialize a session session_start(); // increment a session counter $_SESSION['counter']++; // print value echo "You have viewed this page " . $_SESSION['counter'] . " times"; ?>
为了弄明白这是如何工作的,通过浏览器请求上述脚本几次。你会注意到计数器值在每次随后的页面加载时加1。如果你打开两个浏览器窗口而且在每一个窗口中都请求同一个页面,那么PHP将会为每个浏览器实例维护和累加个自的session计数器。sessionID被用于标识哪个客户端发出了哪个请求,而且为每个单独的session重新创建之前存储的环境。这也意味着如果你在同一个session期间访问另外一个(或多个)Web站点然后返回到上述的脚本而在中间没有关闭你的浏览器,那么你之前的session将会为你被重新检索和创建。

PHP中的每一session都以一个对session_start()函数的调用开始。该函数检查一个session是否已经存在然后或者恢复它(如果存在的话)或者创建一个新的session(如果它不存在的话)。session变量然后可以通过在特殊的超全局数组变量$_SESSION中增加关键字和值而得到注册,然后该变量可以在session期间的任意时候使用标准数组符号被访问。在上面的例子中,一个名字为counter的关键字被增加到$_SESSION数组中。session第一次被创建的时候,该关键字的值为0。在同一session期间对每个后续的页面请求时,计数器之前的值将会被重新得到然后加1。

如果上述例子无法像所说的那样执行,那么请检查以确定你的php.ini文件中的session.save_path变量为你的系统指向了一个有效的临时目录。该值在默认情况下被硬编码为/tmp,因此如果你正在Windows系统下试验该例子,那么你需要将其编辑为C:\Windows\temp(或者你的系统的临时目录)。记住我

这里是另外一个例子,该例子要求你登录然后存储你的登录名称和session起始时间作为两个session变量。然后这个信息被用于显示该session处于活跃期的完整时间。
<?php // initialize a session session_start(); ?> <html> <head></head> <body> <?php if (!isset($_SESSION['name']) && !isset($_POST['name'])) { // if no data, print the form ?> <form action="<?php echo $_SERVER['PHP_SELF']?>" method="post"> <input type="text" name="name"> <input type="submit" name="submit" value="Enter your name"> </form> <?php } else if (!isset($_SESSION['name']) && isset($_POST['name'])) { // if a session does not exist but the form has been submitted // check to see if the form has all required values // create a new session if (!empty($_POST['name'])) { $_SESSION['name'] = $_POST['name']; $_SESSION['start'] = time(); echo "Welcome, " . $_POST['name'] . ". A new session has been activated for you. Click <a href=" . $_SERVER['PHP_SELF'] . ">here</a> to refresh the page."; } else { echo "ERROR: Please enter your name!"; } } else if (isset($_SESSION['name'])) { // if a previous session exists // calculate elapsed time since session start and now echo "Welcome back, " . $_SESSION['name'] . ". This session was activated " . round((time() - $_SESSION['start']) / 60) . " minute(s) ago. Click <a href=" . $_SERVER['PHP_SELF'] . ">here</a> to refresh the page."; } ?> </body> </html>
在该例子中,一个session变量的存在与否用于决定显示三个可能的屏幕中的那一个。session起始时间用返回1970年1月1日和当前时间之间的总秒数的time()函数记录于$_SESSION['start']中。在稍后阶段,存储于$_SESSION['start']的值和最近的time()值比较以计算和显示一个(大约的)耗时展示。

对session_start()函数的调用必须在脚本生成的任意输出(假定你没有使用PHP的输出缓冲特征,你可以在http://www.php.net/manual/en/ref.outcontrol.php中阅读关于输出缓冲的信息)之前首先出现,注意到这一点是重要的。这是因为PHP session处理器内部使用内存cookies以存储session数据,而且cookie创建头必须在任意输出之前传输给客户端浏览器。

如果你曾经在你的session激活的页面中看到过类似下面的错误:
Warning: Cannot send session cache limiter - headers already sent (output started at ...)
该错误通常是因为在session_start()被调用之前在某处,不知何故的,一些输出到达了浏览器。甚至在围绕session_start()函数的PHP标签之外的一个回车或者空格都可以引发这个错误,因此,请密切注意它们。

和之前说明的一样,每个session都有一个唯一的sessionID,PHP使用该sessionID跟踪不同的客户端。这个sessionID是一个长的字符与数字混合的字符串,该字符串被PHP自动地在页面之间传递以使得session的连续性得以保持。为了看它是什么样子的,请使用session_id()函数,就像下面这个例子中一样:
<?php // initialize a session session_start(); // print session ID echo "I'm tracking you with session ID " . session_id(); ?>
当用户关闭客户端浏览器和销毁session时,$_SESSION数组会被冲掉所有的session变量。你也可以通过调用session_destroy()函数明确的销毁一个session(比如,当用户退出的时候),就像下列例子一样:
<?php // initialize a session session_start(); // then destroy it session_destroy(); ?>
倘若你想知道你读的是否正确,那么是的,在你调用session_destroy()来销毁一个session之前,你必须首先调用session_start()来重新创造它。

请记住,$_SESSION是一个超全局变量,因此你在函数内外均可以使用它而不需要首先将其声明为全局变量。下列例子详细解释了这一点:
<?php // initialize a session session_start(); // this function checks the value of a session variable // and returns true or false function isAdmin() { if ($_SESSION['name'] == 'admin') { return true; } else { return false; } } // set a value for $_SESSION['name'] $_SESSION['name'] = "guessme"; // call a function which uses a session variable // returns false here echo isAdmin()."<br />"; // set a new value for $_SESSION['name'] $_SESSION['name'] = "admin"; // call a function which uses a session variable // returns true here echo isAdmin()."<br />"; ?>
你可以在http://www.php.net/manual/en/ref.session.php中阅读更多的关于session和session处理函数的内容。游戏规则

一个session通过使用内存中的cookie来工作,这说明了为什么只在创建session的浏览器实例运行时session是有效的;一旦浏览器实例被终止,那么分配给它的内存就会被冲刷掉然后返回给系统,同时销毁进程中的session cookie。如果你想要更持久的cookies,那么你可以使用PHP内建的cookie函数将数据以cookie文件的形式写入到用户磁盘,然后当需要的时侯将该数据读回。

在你开始使用cookies之前,有一些事情你需要清楚:

1. 因为cookies被用于记录你在某特定域上活动的信息,所有它们只可以被创建它们的域来读取。
2.一个域无法设置超过20个的cookies而且每个cookie限于最大4 KB的大小。
3. 一个cookie通常处理六个属性,其中,只有第一个是强制性的。下面就是这六个属性:
o 名称: cookie的名称
o 值:cookie的值
o 过期时间:cookie过期的日期和时间
o 路径:cookie数据可以被访问的域的最顶层目录。
o 域:cookie有效的域
o 安全:一个指示cookie是否应该只通过安全的HTTP连接传输的布尔标志。

更多的关于cookies的信息可以从NetScape公司获得,该公司首先发明了cookies。访问http://www.netscape.com/newsref/std/cookie_spec.html可以获得NetScape cookie的规范。

因为cookies存储于用户的硬盘上,所以作为开发人员的你几乎无法控制它们,记住这一点是重要的。如果一个用户决定取消其浏览器对cookie的支持,那么你的cookies将只是不被保存。因此,请避免编写严重依赖cookies的代码;而且在cookie数据无法从客户端获取时做好后备计划。

不要管上述警告,让我们看看PHP中的一些简单的cookie处理代码。

偶遇老朋友

PHP提供了单一的用于cookie操作的函数:setcookie()。该函数允许你读写cookie文件,就像下面例子演示的一样:
<?php if (!isset($_COOKIE['visited'])) { // if a cookie does not exist // set it setcookie("visited", "1", mktime()+86400, "/") or die("Could not set cookie"); echo "This is your first visit here today."; } else { // if a cookie already exists echo "Nice to see you again, old friend!"; } ?>
为了理解这是如何工作的,请通过浏览器请求上述页面几次。第一次请求时,因为依然没有cookie被设置,所以第一条消息被显示。在所有后面的尝试中,因为cookie已经被设置了,所以客户端将被识别然后第二条消息被显示。请注意,即使你终止了浏览器实例,重启浏览器然后再次访问之前的页面,这个依然有用(一个不同于你之前看到的session例子中所做的明显差异)。

setcookie()函数接受六个参数:cookie名称、cookie值、cookie有效期、cookie域、cookie有效路径以及指示其安全状态的布尔值。正如之前所注解的那样,只有名称和值是强制的,即使上述例子使用mktime()函数(它工作的方式类似于之前所描述的time()函数)指定cookie的顶层目录和有效期(1天),
Cookie值被自动地从客户端送到PHP,然后被转化为类似于$_SESSION的超全局数组的$_COOKIE变量中的关键字-值对。值可以使用标准的关联数组符号来取得,就像上述例子一样。请注意,和session一样,对setcookie()的调用必须发生在脚本产生任意输出之前,否则你会看到像下面这样的错误:
Warning: Cannot add header information - headers already sent by (output started at ... )表单和函数

下面是另外一个稍微复杂一些的例子:
<?php if (!isset($_POST['email'])) { // if form has not been submitted // display form // if cookie already exists, pre-fill form field with cookie value ?> <html> <head></head> <body> <form action="<?php echo $_SERVER['PHP_SELF']?>" method="post"> Enter your email address: <input type="text" name="email" value="<?php echo $_COOKIE['email']; ?>"> <input type="submit" name="submit"> <?php // also calculate the time since the last submission if ($_COOKIE['lastsave']) { $days = round((time() - $_COOKIE['lastsave']) / 86400); echo "<br /> $days day(s) since last submission"; } ?> </form> </body> </html> <?php } else { // if form has been submitted // set cookies with form value and timestamp // both cookies expire after 30 days if (!empty($_POST['email'])) { setcookie("email", $_POST['email'], mktime()+(86400*30), "/"); setcookie("lastsave", time(), mktime()+(86400*30), "/"); echo "Your email address has been recorded."; } else { echo "ERROR: Please enter your email address!"; } } ?> </body> </html>
在这个例子中,输入表单的值被存储为一个称为email的cookie,然后在所有后续请求中,该值被自动获取以提前填充表单。该技术经常被需要用户输入登录名称和密码的Web站点来使用;通过在登录窗口用之前用户成功尝试的值自动提前填充用户名字段,它们为用户减少了一些按键操作。
这个例子也展示了你如何通过调用setcookie()多次为一个域设置超过一个的cookie。在上面的例子中,数据输入的时间被作为第二个cookie进行存储,然后用于计算两个连续登录之间的时间。

为了从客户端移除一个cookie,只要使用和你最初设置cookie时所用的语法相同的语法(除了过去的有效期之外)来调用setcookie()即可。这会引起cookie从客户端系统被移除。下面是一个例子:
<?php // delete cookie setcookie("lastsave", NULL, mktime() - 3600, "/"); ?>
阅读更多的关于cookies和setcookie()函数的信息,请访问:
http://www.php.net/manual/en/features.cookies.php 和 http://www.php.net/manual/en/function.setcookie.php访问授权

正如我在本教程开始之前所讲的那样,cookies和sessions是两个截然不同的使得数据在客户端“持久化”的方法。session在session期间保持数据,而cookie可以保持数据到你需要的长的时间。请记住这一点,让我们现在看一个同时使用了它们两个的例子。

这里的应用是一个简单的用户认证系统,在该系统里,某些页面只能被成功登录系统的用户查看。没有通过有效密码验证的用户被拒绝访问这些“特殊”页面。有效的用户名和密码列表存储于一个MySQL数据库中,而PHP被用于验证用户的身份然后决定是否授权访问。

假定MySQL数据库表如下面所示一样
+-------+-----------------------------------------------+ | name | pass | +-------+-----------------------------------------------+ | sue | 9565d44fd0fe4db59f073eea1db70f3ea258e10b | | harry | 6e74234b8b552685113b53c7bff0f386c8cef8cf | | louis | 6817dda51b64b8190029490d2811a4d9cb9cd432 | | sam | bd17f8243e771a57cfbb06aa9a82bbf09fd2d90b | | james | 792ec9b44d432c947ac6775b2b52326e9d08512f | +-------+-----------------------------------------------+
有唯一的用户名字段和使用SHA1()函数创建的密码字段,下面是做所有这些艰苦工作的PHP脚本:

这里,输入登录框的值被结合到MySQL SELECT查询语句中,该语句在用户表上执行。如果用户名和密码两个都匹配,那么唯一一个记录将会被返回,这意味着认证成功;如果它们不匹配,没有记录会被返回,这意味着认证失败。
假定认证成功,session被初始化,$_SESSION['auth']关键字被创建和被赋布尔真值,然后用户名被存储于cookie中以备下次使用。这个cookie将在30天内保持有效,而且被用于下次登录尝试时在登录框内提前填充用户名字段。
当然,这个本身还不是充分的。虽然上面的脚本执行认证然后在用户证明有效时初始化一个session和一个cookie,但是安全检查必须也在每个受限页面上执行。没有这个安全检查,任何用户都可以绕过该登录界面而且只要简单输入每个页面的准确的URL就可查看该页面。
如果用户证明被确认,那么session变量$_SESSION['auth']才可以存在,这一点在上面脚本中是清楚的,正因为如此,在每一受限页面的开始检查变量$_SESSION['auth']的存在与否而且如果该检查返回真则授权访问,这就足够了。下面的例子解释了其是如何实现的:
<?php // start session session_start(); if (!$_SESSION['auth'] == 1) { // check if authentication was performed // else die with error die ("ERROR: Unauthorized access!"); } else { ?> <html> <head></head> <body> This is a secure page. You can only see this if $_SESSION['auth'] = 1 </body> </html> <?php } ?>
非常整洁?只有认证过的用户能够看到这个页面,因为只有这些用户的客户端才会有一个内有$_SESSION['auth']变量的session。其他认证的用户只看到一个错误的消息。

那就是本教程的所有内容。在第十一章,我将告诉你所有的关于SimpleXML---这个和PHP5一起绑定提供的新的XML处理工具箱。请一定要为了它回来!
0
相关文章