cookie和会话之间的关键区别是,cookie将数据存储在用户的浏览器里,而会话把数据存储在服务器上面。会话一般比cookie更安全,并且可以存储多得多的信息。这两种技术都很容易与PHP一起使用,并且值得了解。在本章中,你将看到cookie和会话的使用。演示该信息的示例将是一个登录系统,它基于现有的users数据库。
12.4 使用会话
使数据可供Web站点上的多个页面使用的另一种方法是使用会话(session)。会话假定数据存储在服务器上,而不是在Web浏览器中,会话标识符用于定位特定用户的记录(会话数据)。这个会话标识符通常通过cookie存储在用户的Web浏览器中,但是,敏感数据本身(如用户的ID、姓名等)总是保留在服务器上。
有人可能会问:本来cookie工作得好好的,为什么还要使用会话?首先,会话更安全,这是由于所有记录的信息都存储在服务器上,并且不会持续不断地在服务器和客户之间来回发送。其次,可以在会话中存储更多的数据。最后,有些用户拒绝cookie,或者完全关闭它们。虽然会话被设计成与cookie一起工作,但它也可以独立起作用。
为了演示会话,并把它们与cookie作比较,我将重写以前的脚本集。
会话与cookie
本章具有一些示例,它们使用cookie和会话来完成相同的任务(登录和注销)。显然,它们都能很容易地在PHP中使用,但是,真正的问题是什么时候使用哪一个更合适。
与cookie相比,会话具有以下优点:
一般更安全(因为数据保存在服务器上);
允许存储更多数据;
使用会话时,可以不使用cookie。
与会话相比,cookie则具有以下优点:
更容易编程;
需要更少的服务器资源;
通常情况下能够持续更长时间。
一般而言,要存储和检索少量的信息,则可使用cookie。不过,对于大部分Web应用程序,都会使用会话。
12.4.1 设置会话变量
关于会话的最重要的规则是,将会使用它们的每个页面首先都必须调用session_start()函数。这个函数告诉PHP开启一个新的会话,或者访问一个现有的会话。必须在把任何内容发送到Web浏览器之前调用这个函数。
第一次使用session_start()函数时,它会试图发送一个cookie,名称为PHPSESSID(会话名称),和一个类似于a61f8670baa8e90a30c878df89a2074b(32个十六进制字母,它是会话ID)的值。由于试图发送一个cookie,所以在把任何数据发送到Web浏览器之前,必须先调用session_start(),这与使用setcookie()和header()这两个函数时的情况一样。
一旦启动了会话,就可以使用正常的数组语法把值注册到会话中:
$_SESSION[‘key’] = value;
$_SESSION[‘name’] = ‘Roxanne’;
$_SESSION[‘id’] = 48;
记住这一点后,让我们更新login.php脚本。
开启会话
(1)在文本编辑器或IDE中打开login.php(参见脚本12-5)。
(2)用以下几行代码替换setcookie()那几行代码(第18~19行)(参见脚本12-8)。
脚本12-8 login.php脚本现在使用的是会话,而不是cookie
1 <?php # Script 12.8 – login.php #3
2 // This page processes the login form submission.
3 // The script now uses sessions.
4
5 // Check if the form has been submitted:
6 if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’) {
7
8 // Need two helper files:
9 require (‘includes/login_functions.inc.php’);
10 require (‘../mysqli_connect.php’);
11
12 // Check the login:
13 list ($check, $data) = check_login($dbc, $_POST[’email’], $_POST[‘pass’]);
14
15 if ($check) { // OK!
16
17 // Set the session data:
18 session_start();
19 $_SESSION[‘user_id’] =$data’user_id’];
20 $_SESSION[‘first_name’] =$data[‘first_name’];
21
22 // Redirect:
23 redirect_user(‘loggedin.php’);
24
25 } else { // Unsuccessful!
26
27 // Assign $data to $errors for login_page.inc.php:
28 $errors = $data;
29
30 }
31
32 mysqli_close($dbc); // Close the database connection.
33
34 } // End of the main submit conditional.
35
36 // Create the page:
37 include (‘includes/login_page.inc.php’);
38 ?>
第一步是开启会话。因为在脚本中这一刻之前没有echo()语句、HTML文件包含,或者甚至是空白,所以现在可以安全地使用session_start()(尽管也可以把它放在脚本的顶部)。然后,把两个键—值(key-value)对添加到$_SESSION超全局数组中,以把用户的名字和用户ID注册到会话中。
(3)将页面另存为login.php,存放在Web目录中,并在Web浏览器中测试它(参见图12-17)。
图12-17 登录表单对最终用户保持不变,但是底层的功能现在使用的是会话
尽管需要重写loggedin.php以及头部和脚本,你仍然可以测试登录脚本,并查看得到的cookie(参见图12-18)。不过,loggedin.php页面应该把你重定向回主页,因为它仍会检查$_COOKIE变量存在与否。
图12-18 由PHP的session_start()函数创建的这个cookie存储会话ID
√提示
由于会话通常会发送和读取cookie,应该总是设法在脚本中尽可能早地开启它们。这样做将有助于避免如下问题:试图在已经发送头部(HTML或空白)之后发送cookie。
如果你愿意,可以在php.ini文件中把session.auto_start设置为1,从而不必在每个页面上使用session_start()。这样做会加大服务器上的开销,由于这个原因,如果不考虑某些环境因素,就不应该使用它。
可以在会话中存储数组(使$_SESSION成为一个多维数组),就像可以存储字符串或数字一样。
12.4.2 访问会话变量
一旦启动了会话,并向其注册了变量,就可以创建将访问这些变量的其他脚本。为了执行该操作,每个脚本首先必须再次使用session_start()来启用会话。
这个函数将允许当前脚本访问以前启动的会话(如果它可以读取cookie中存储的PHPSESSID值),或者创建一个新的会话(如果它不能读取这个值)。要理解如果不能找到当前会话ID或者生成了新的会话ID,那么在旧会话ID下存储的所有数据都将不可用。我之所以现在在这里提到这一点,是因为如果你有关于会话的问题,那么第一个调试步骤是检查会话ID值,看看它是否因页面而异。
假定访问当前会话没有问题,要引用一个会话变量,可使用$_SESSION[‘var’],就像你引用任何其他数组一样。
访问会话变量
(1)在文本编辑器或IDE中打开loggedin.php(参见脚本12-4)。
(2)添加对session_start()函数的调用(参见脚本12-9)。
脚本12-9 我更新了loggedin.php脚本,使得它可以引用$_SESSION,而不是引用$_COOKIE(需要在两行上执行更改)
1 <?php # Script 12.9 – loggedin.php #2
2 // The user is redirected here from login.php.
3
4 session_start(); // Start the session.
5
6 // If no session value is present, redirect the user:
7 if (!isset($_SESSION[‘user_id’])) {
8
9 // Need the functions:
10 require (‘includes/login_functions.inc.php’);
11 redirect_user();
12
13 }
14
15 // Set the page title and include the HTML header:
16 $page_title = ‘Logged In!’;
17 include (‘includes/header.html’);
18
19 // Print a customized message:
20 echo “<h1>Logged In!</h1>
21 <p>You are now logged in, {$_SESSION [‘first_name’]}!</p>
22 <p><a href=\”logout.php\”>Logout</a></p>”;
23
24 include (‘includes/footer.html’);
25 ?>
脚本12-10 header.html文件现在也会引用$_SESSION,而不是引用$_COOKIE
1 <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/
xhtml1-strict.dtd”>
2 <html >
3 <head”>
4 <title><?php echo $page_title; ?> </title>
5 <link rel=”stylesheet” href=”includes/style.css” type=”text/css” media=”screen” />
6 <meta http-equiv=”content-type” content=”text/html; charset=utf-8″ />
7 </head>
8 <body>
9 <div id=”header”>
10 <h1>Your Website</h1>
11 <h2>catchy slogan…</h2>
12 </div>
13 <div id=”navigation”>
14 <ul>
15 <li><a href=”index.php”>Home Page</a></li>
16 <li><a href=”register.php”>Register</a></li>
17 <li><a href=”view_users.php”>View Users</a></li>
18 <li><a href=”password.php”>Change Password</a></li>
19 <li><?php // Create a login/logout link:
20 if (isset($_SESSION[‘user_id’])) {
21 echo ‘<a href=”logout.php”>Logout</a>’;
22 } else {
23 echo ‘<a href=”login.php”>Login</a>’;
24 }
25 ?></li>
26 </ul>
27 </div>
28 <div id=”content”><!– Start of the page-specific content. –>
29 <!– Script 12.10 – header.html –>
为了让Login/Logout链接正确地工作(注意图12-17中不正确的链接),必须把头文件内对cookie变量的引用转变成会话。头文件不需要调用session_start()函数,因为它会被调用该函数的页面包括在内。
√提示
为了让Login/Logout链接在其他页面(register.php、index.php等)上也能正确工作,将需要在其中的每一个页面上添加session_start()命令。
回忆一下我曾经说过的,如果你具有一个应用程序,它看起来似乎不能从一个页面到另一个页面访问会话数据,这可能是由于新的会话是在每个页面上创建的。为了检查这一点,可以比较会话ID(值的最后几个字符就足够了)来查看它是否相同。你可以在发送会话cookie时通过查看会话cookie来查看会话的ID,或者使用session_id()函数来执行该操作:
echo session_id();
一旦建立了会话变量,就可以使用它们。因此,与使用cookie时不同,可以把一个值赋予$_SESSION[‘var’],然后在同一个脚本的后面引用$_SESSION[‘var’]。
垃圾收集
关于会话的垃圾收集是指删除会话文件(其中存储了实际的数据)的过程。创建一个销毁会话的注销系统是理想的,但是,并不能保证所有用户都按应该做的那样正式注销。所以PHP包含了一个清理进程。
无论何时调用session_start()函数,都会引入PHP的垃圾收集,用于检查每个会话最近的修改日期(每当设置或获取变量时都会修改会话)。有两个设置规定了垃圾收集,它们是:session.gc_maxlifetime和session.gc_probability。第一个设置用于指定在一个会话持续多少秒不活动之后,可将其看作空闲会话,从而删除。第二个设置确定执行垃圾收集的概率,其取值范围为1~100。因此,默认设置是,对session_start()的每个调用都有1%的机会调用垃圾收集。如果PHP没有启动清理进程,则会删除任何超过1440秒未使用的会话。
你可以使用ini_set()函数改变这些设置,尽管要小心谨慎地执行该操作。频率过高或概率太大的垃圾收集可能使服务器陷入困境,并且会不经意地结束较慢用户的会话。
12.4.3 删除会话变量
在使用会话时,需要创建一个方法来删除会话数据。在当前示例中,当用户注销时,这是必要的。
虽然cookie系统只需要发送另一个cookie来销毁现有的cookie,但是会话的要求更高,因为既要考虑客户上的cookie,又要考虑服务器上的数据。
要删除单独一个会话变量,可以使用unset()函数(它可以处理PHP中的任何变量):
unset($_SESSION[‘var’]);
要删除每个会话变量,可以重置整个$_SESSION数组:
$_SESSION = array();
最后,要从服务器中删除所有的会话数据,可使用session_destroy():
session_destroy();
注意,在使用其中的任何一个方法之前,页面都必须开始于session_start(),使得会访问现有的会话。让我们更新logout.php脚本,以清理会话数据。
删除会话
(1)在文本编辑器或IDE中打开logout.php(参见脚本12-6)。
(2)紧接在开始PHP行之后启动会话(参见脚本12-11)。
脚本12-11 销毁会话(就像在注销页面中所做的那样)需要使用特殊的语法,以删除服务器上的会话cookie和会话数据,以及清理$_SESSION数组
1 <?php # Script 12.11 – logout.php #2
2 // This page lets the user logout.
3 // This version uses sessions.
4
5 session_start(); // Access the existing session.
6
7 // If no session variable exists, redirect the user:
8 if (!isset($_SESSION[‘user_id’])) {
9
10 // Need the functions:
11 require (‘includes/login_functions.inc.php’);
12 redirect_user();
13
14 } else { // Cancel the session:
15
16 $_SESSION = array(); // Clear the variables.
17 session_destroy(); // Destroy the session itself.
18 setcookie (‘PHPSESSID’, ”, time()-3600, ‘/’, ”, 0, 0); // Destroy the cookie.
19
20 }
21
22 // Set the page title and include the HTML header:
23 $page_title = ‘Logged Out!’;
24 include (‘includes/header.html’);
25
26 // Print a customized message:
27 echo “<h1>Logged Out!</h1>
28 <p>You are now logged out!</p>”;
29
30 include (‘includes/footer.html’);
31 ?>
无论何时使用会话,都必须使用session_start()函数,最好是在页面顶部这样做,即使删除会话也是如此。
(3)更改条件语句,使得它检查会话变量是否存在。
if (!isset($_SESSION[‘user_id’])) {
与cookie示例中的logout.php脚本一样,如果用户目前没有登录,就会重定向他们。
(4)用下面的几行代码替换setcookie()行(它删除cookie)。
$_SESSION = array();
session_destroy();
setcookie (‘PHPSESSID’, ”, time()-3600, ‘/’, ”, 0, 0);
这里的第一行代码将把整个$_SESSION变量重置为一个新数组,从而清除其现有的值。第二行代码从服务器中删除数据,第三行代码则会发送一个cookie,用以替换浏览器中现有的会话cookie。
(5)删除消息中对$_COOKIE的引用。
echo “<h1>Logged Out!</h1>
<p>You are now logged out!</p>”;
与使用logout.php脚本的cookie版本时不同,不能再通过用户的名字来引用他们,因为所有这类数据都已被删除。
(6)将文件另存为logout.php,存放在Web目录中,并在浏览器中测试它(参见图12-21)。
图12-21 注销页面(现在具有会话)
√提示
header.html文件只需要检查$_SESSION[‘user_id’]是否已经设置,或者没有设置如果页面是登出页面,因为现在头部文件被logout.php所包含,所有的会话(session)数据将已经被销毁了。会话的数据会被立即销毁,不像cookies那样。
永远不要把$_SESSION设置成等于NULL并且永远不要使用unset($_SESSION),因为它们都可能会在某些服务器上引发问题。
以免你对接下来将要发生的事情不是绝对清楚,提供了关于会话的三类信息:会话标识符(它默认存储在cookie中)、会话数据(它存储在服务器上的一个文本文件中)和$_SESSION数组(它指定了脚本访问文本文件中的会话数据的方式)。仅仅删除cookie不会删除文本文件,反之亦然。清理$_SESSION数组将会清除文本文件中的数据,但是文件本身将仍然存在,cookie也是如此。这个注销脚本中概括的三个步骤实际上会删除会话的所有痕迹。
更改会话行为
作为PHP对会话所提供支持的一部分,可以为PHP处理会话的方式设置大约20种不同的配置选项。有关完整列表,参见PHP手册,但是我将在这里重点介绍几个最重要的选项。注意关于更改会话设置的两条规则:
(1)所有更改都必须在调用session_start()之前完成。
(2)必须在使用会话的每个页面执行相同的更改。
可以在PHP脚本内使用ini_set()函数(在第7章中讨论过)设置大多数选项:
ini_set (parameter, new_setting);
例如,如果需要使用会话cookie(如前所述,在不使用cookie的情况下会话也可以工作,但是这样不太安全),则可使用:
ini_set (‘session.use_only_cookies’, 1);
还可以更改会话的名称(也许是为了使用一个更为用户友好的名称),这时可以使用session_name()函数。
session_name(‘YourSession’);
创建自己的会话名称有双重好处:它更安全一点,并且可以更好地被最终用户接收(因为会话名称是最终用户将使用的cookie名称)。在删除会话cookie时,也可以使用session_name()函数:
setcookie (session_name(), ”, time()-3600);
最后,还有一个session_set_cookie_params()函数。它用于调整会话cookie的设置。
session_set_cookie_params(expire, path, host, secure, httponly);
注意:cookie的到期时间只指cookie在Web浏览器中的寿命,而不是指会话数据将在服务器上存储多长时间。
12.5 提高会话安全性
由于重要的信息通常存储在会话中(永远都不应该把敏感数据存储在cookie中),所以安全性变得更重要。关于会话需要注意以下两项:会话ID和会话数据本身,前者是一个指向会话数据的引用,后者存储在服务器上。一个不怀好意的人极有可能通过会话ID(而不是通过服务器上的数据)来入侵一个会话,因此,在这里将重点关注这方面的事情(在本节末尾的“提示”中,我提到了保护会话数据的两种方式)。
会话ID是会话数据的关键。默认情况下,PHP将其存储在cookie中,从安全的角度讲,这是首选的方法。在PHP中可以在不使用cookie的情况下使用会话,但是这会使应用程序遭受会话劫持(sessionhi jacking):如果我可以获悉另一个用户的会话ID,就可以轻松地欺骗服务器把它看作是我的会话ID。此时,我就有效地接管了原用户的整个会话,并且可以访问他们的数据。因此,把会话ID存储在cookie中使得它更难被窃取。
防止劫持的一种方法是:在会话中存储某种用户标识符,然后反复地复查这个值。HTTP_USER_AGENT(所用的浏览器和操作系统的组合)是针对此目的的一个可能的候选。这会增加一层安全性,因为仅当我运行的浏览器和操作系统与另一位用户的完全一样时,才能够劫持他的会话。为了演示这一点,让我们最后一次修改示例。
防止会话固定
另一种特定的会话攻击被称为会话固定(session fixation)。其中,一位不怀好意的用户指定了另一位用户应该使用的会话ID。这个会话ID可以是随机生成的,或者是合法创建的。在这两种情况下,真实的用户都会使用固定的会话ID进入站点,并做任何事情。然后,那位不怀好意的用户可以访问那个会话,因为他们知道会话ID是什么。你可以在用户登录后通过更改会话ID来帮助防止这些类型的攻击。session_regenerate_id()正是用于执行该任务的,它提供一个新的会话ID来引用当前的会话数据。如果站点的安全性极为重要(如电子商务或在线银行业务),或者如果用户的会话被操纵情况就变得特别糟糕时,就可以使用这个函数。
更安全地使用会话
(1)在文本编辑器或IDE中打开login.php(参见脚本12-8)。
(2)在给其他会话变量赋值之后,还存储HTTP_USER_AGENT值(参见脚本12-12)。
脚本12-12 login.php脚本的这个最终版本也在会话中存储了用户的HTTP_USER_AGENT(客户的浏览器和操作系统)的加密形式
1 <?php # Script 12.12 – login.php #4
2 // This page processes the login form submission.
3 // The script now stores the HTTP_USER_AGENT value for added security.
4
5 // Check if the form has been submitted:
6 if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’) {
7
8 // Need two helper files:
9 require (‘includes/login_functions.inc.php’);
10 require (‘../mysqli_connect.php’);
11
12 // Check the login:
13 list ($check, $data) = check_login($dbc, $_POST[’email’], $_POST[‘pass’]);
14
15 if ($check) { // OK!
16
17 // Set the session data:
18 session_start();
19 $_SESSION[‘user_id’] = $data[‘user_id’];
20 $_SESSION[‘first_name’] = $data[‘first_name’];
21
22 // Store the HTTP_USER_AGENT:
23 $_SESSION[‘agent’] = md5($_SERVER [‘HTTP_USER_AGENT’]);
24
25 // Redirect:
26 redirect_user(‘loggedin.php’);
27
28 } else { // Unsuccessful!
29
30 // Assign $data to $errors for login_page.inc.php:
31 $errors = $data;
32
33 }
34
35 mysqli_close($dbc); // Close the database connection.
36
37 } // End of the main submit conditional.
38
39 // Create the page:
40 include (‘includes/login_page.inc.php’);
41 ?>
HTTP_USER_AGENT是$_SERVER数组的一部分(你可以返回到第1章,回忆一下使用它的方式)。它将具有一个像Mozilla/4.0这样的值(与之兼容的值、MSIE 8.0、Windows NT 6.1等)。
为了提高安全性,这里没有把这个值存储在会话中,而是用md5()函数处理它。该函数基于一个值返回32个字符的十六进制字符串,称为散列(hash)。理论上讲,任何两个字符串都不具有相同的md5()结果。
(3)保存文件,存放在Web目录中。
(4)在文本编辑器或IDE中打开loggedin.php(参见脚本12-9)。
(5)将!isset($_SESSION[‘user_id’])条件语句更改如下(参见脚本12-13):
脚本12-13 这个loggedin.php脚本现在确认访问这个页面的用户具有与他们登录时相同的HTTP_USER_AGENT
1 <?php # Script 12.13 – loggedin.php #3
2 // The user is redirected here from login.php.
3
4 session_start(); // Start the session.
5
6 // If no session value is present, redirect the user:
7 // Also validate the HTTP_USER_AGENT!
8 if (!isset($_SESSION[‘agent’]) OR ($_SESSION[‘agent’] != md5($_SERVER [‘HTTP_USER_AGENT’]) )) {
9
10 // Need the functions:
11 require (‘includes/login_functions.inc.php’);
12 redirect_user();
13
14 }
15
16 // Set the page title and include the HTML header:
17 $page_title = ‘Logged In!’;
18 include (‘includes/header.html’);
19
20 // Print a customized message:
21 echo “<h1>Logged In!</h1>
22 <p>You are now logged in, {$_SESSION
[‘first_name’]}!</p>
23 <p><a href=\”logout.php\”>Logout</a></p>”;
24
25 include (‘includes/footer.html’);
26 ?>
这个条件语句用于检查两件事情。首先,它会查看$_SESSION[‘agent’]变量是否未设置(这一部分就像它以前一样,尽管使用的是agent而不是user_id)。这个条件语句的第二部分用于检查$_SERVER [‘HTTP_ USER_AGENT’]的md5()版本是否不等于$_SESSION [‘agent’]中存储的值。如果这两个条件中有一个为真,就会重定向用户。
(6)保存这个文件,存放在Web目录中,并通过登录在Web浏览器中测试它。
√提示
对于至关重要的会话应用,只要有可能,就需要使用cookie并通过安全连接传输它们。你甚至可以通过把session.use_only_cookies设置成1,将PHP设置成只使用cookie。
如果你使用与其他域共享的服务器,那么把session.save_path从其默认设置(可以被所有用户访问)更改成稍微更本地化一些将会更安全。
会话数据本身可以存储在数据库中,而不是文本文件中。这样更安全,但这是一个编程密集的选项。我在PHP 5 Advanced: Visual QuickPro Guide一书中讲述了如何执行该任务。
用户的IP地址(用户通过其建立连接的网络地址)不是一个良好的唯一标识符,这有两个原因。首先,用户的IP地址可能(并且通常会)频繁地发生变化(ISP在短时间内动态地分配它们)。其次,从同一个网络(如家庭网络或办公室)访问一个站点的许多用户可能都具有相同的IP地址。