PHP使用cookie和会话

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地址。