Drupal专业开发指南 第16章 会话

 

HTTP是一个无状态协议,这意味着浏览器和服务器之间的每次交互都是相互独立的。当一个用户访问一个网站的一系列页面时,你该如何对其进行追踪呢?你可以使用会话。从PHP4开始,通过会话家族函数,PHP提供了对会话的内置支持。在本章,你将看到Drupal是如何使用PHP会话的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 什么是会话?

老葛的Drupal培训班 Think in Drupal

当一个浏览器第一次请求一个Drupal站点的页面时,PHP会向浏览器发送一个cookie,里面包含了一个随机生成的32位字符串ID,默认称为PHPSESSID。在浏览器第一次访问一个站点时,在返回的HTTP响应头部中包含了一行信息,它就是cookie,如下所示:
 
HTTP/1.1 200 OK
Date: Thu, 17 Apr 2008 20:24:58 GMT
Server: Apache
Set-Cookie: PHPSESSID=3sulj1mainvme55r8udcc6j2a4; expires=Sat, 10 May 200823:58:19
GMT; path=/
Last-Modified: Thu, 17 Apr 2008 20:24:59 GMT
Cache-Control: store, no-cache, must-revalidate
Cache-Control: post-check=0, pre-check=0
Content-Type: text/html; charset=utf-8
 
    在接下来的访问中,浏览器将cookie包含在每个HTTP请求中,用来向服务器出示cookie:
 
GET / HTTP/1.1
User-Agent=Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.14)
Gecko/20080404 Firefox/2.0.0.14
Cookie: PHPSESSID=3sulj1mainvme55r8udcc6j2a4
 

    这样当单个浏览器请求网站页面时,PHP就可以对其进行追踪了。32位字符串ID,也就是会话ID,它有两种功能,首先它可以作为键来读取Drupal存储的会话信息,另外它允许Drupal将会话与单独的用户关联起来。

Drupal版本:

Drupal专业开发指南 第16章 用法

老葛的Drupal培训班 Think in Drupal

Drupal在多个重要的内部函数中,使用会话来存储临时信息,比如单个用户的状态或偏爱。例如,drupal_set_message()需要为用户传递状态消息或者错误消息,将消息从错误发生的页面传递到下一个页面中。在这里,我们将消息存储在用户会话中的一个名为messages的数组里面,从而完成传递。
 
/**
 * Set a message which reflects the status of the performed operation.
 *
 * If the function is called with no arguments, this function returns all set
 * messages without clearing them.
 *
 * @param $message
 * The message should begin with a capital letter and always ends with a
 * period '.'.
 * @param $type
 * The type of the message. One of the following values are possible:
 * 'status', 'warning', 'error'
 * @param $repeat
 * If this is FALSE and the message is already set, then the message won't
 * be repeated.
 */
function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
    if ($message) {
        if (!isset($_SESSION['messages'])) {
            $_SESSION['messages'] = array();
        }
        if (!isset($_SESSION['messages'][$type])) {
            $_SESSION['messages'][$type] = array();
        }
        if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
            $_SESSION['messages'][$type][] = $message;
        }
    }
    // Messages not set when DB connection fails.
    return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
}
 
 另一个例子来自于comment.module,在这里使用会话为匿名用户存储查看偏爱:
 
$_SESSION['comment_mode'] = $mode;
$_SESSION['comment_sort'] = $order;
$_SESSION['comment_comments_per_page'] = $comments_per_page;
 
  在预览节点时,Drupal使用会话来处理文件上传。在“管理➤内容管理➤内容”中过滤站点内容列表时,Drupal用会话记住用户浏览偏爱,还有在“管理➤报告➤最新的日志条目”中过滤最新日志条目列表时,用的也是会话。而在安装和更新系统中,也使用了会话(install.php和update.php)。
    对于登录用户和匿名用户,Drupal都为其都创建了会话。在表sessions中有一行表示匿名用户的记录,其中uid列被设置为了0.由于会话是特定于浏览器的(它们绑定在了浏览器的cookie上),这样,在单个计算机上打开多个浏览器就会有多个会话。
 
 
警告 在匿名用户初次访问一个站点时,Drupal并不为其存储会话信息。这样当恶意的网络爬虫和机器人访问站点时,就不会向sessions表中写入大量数据了。对于开发者来说,这意味着你不能为匿名用户的初次访问,存储会话信息。
 
    存储在会话中的实际数据,是作为序列化数据存储在sessions表的session列中的。表16-1给出来一个典型的sessions表,它包含了3条记录。对于超级用户(uid 1),认证用户(uid 3),匿名用户(uid 0),都各有一条记录。对于超级用户,在其会话中,也就是session列,存储了看门狗过滤设置(用在dblog模块)。
 
16-1.来自sessions表的示例记录
uid    sid                      hostname  timestamp cache session
1 f5268d678333a1a7cce27e7e42b0c2e1 1.2.3.4      1208464106 0 dblog_overview_
                                                                filter|a:0:{}
3 be312e7b35562322f3ee98ccb9ce8490 5.6.7.8      1208460845 0  --
0 5718d73975456111b268ed06233d36de 127.0.0.1    1208461007 0  --
 
    当PHP的会话垃圾收集器周期性运行时,就会清理sessions表。记录在表中的保留时限由settings.php中的session.gc.maxlifetime决定。如果用户退出,那么与之相关的会话记录将被立即从数据库中清除。注意,如果一个用户通过多个浏览器(而不是浏览窗口)或者多个IP地址同时登录,那么每个浏览器都有一个会话;因此,从一个浏览器退出不会影响用户在其它浏览器的登录状态。

Drupal版本:

Drupal专业开发指南 第16章 会话相关设置

 

3个地方,可以修改Drupal的会话设置:.htaccess文件, settings.php文件, includes/bootstrap.inc文件中的引导指令代码。
 
在.htaccess文件中
    在Drupal安装的默认文件.htaccess中,使用下面一行代码关闭PHP的session.auto_start功能,这样在会话启动后,Drupal就确保拥有完全的控制能力了。代码如下:
 
php_value session.auto_start                0
 
    session.auto_start是一个配置选项,由于PHP在运行时不能改变它,所以将其放置在.htaccess文件中而不是settings.php中。
 
在settings.php文件中
    在settings.php文件中,你可以设置大部分的会话设置,该文件位于sites/default/
settings.php或者sites/example.com/settings.php。
 
ini_set('session.cache_expire',     200000); // 138.9 days
ini_set('session.cache_limiter',    'none');
ini_set('session.cookie_lifetime', 2000000); // 23.1 days
ini_set('session.gc_maxlifetime',   200000); // 55 hours
ini_set('session.save_handler',    'user'); // Use user-defined session handling.
ini_set('session.use_only_cookies',     1); // Require cookies.
ini_set('session.use_trans_sid',        0); // Don't use URL-based sessions.
 
    将这些设置放在settings.php中而不是.htaccess文件中,这就可以使子站点可以拥有不同的设置,并且在PHP作为CGI运行时(.htaccess中的PHP指令不能在这样的配置中工作),允许Drupal修改会话设置。
    Drupal使用函数ini_set('session.save_handler', 'user');来覆写由PHP提供的默认会话处理器,并实现它自己的会话管理。user-defined在这里的意思是“由Drupal定义的”(参看http://www.php.net/manual/en/function.session-set-save-handler.php)。
 
在bootstrap.inc文件中
    PHP提供了内置的会话处理函数,如果你想实现自己的会话处理器,那么PHP允许你覆写这些函数。这样PHP仍然负责cookie管理,而Drupal的实现则负责后台的会话存储。
    在引导指令中的DRUPAL_BOOTSTRAP_SESSION阶段,下面的代码将处理器设置为了includs/session.inc里面的函数,并启动会话处理:
 
require_once variable_get('session_inc', './includes/session.inc');
session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write',
'sess_destroy_sid', 'sess_gc');
session_start();
 
    在个别的时候,一个文件内部的函数名可以不使用文件名作为前缀,这里就是这样。你可能会认为前面的函数应该为session_open,session_close等等。然而,由于PHP在该命名空间下已经有了这些函数,所以这里使用了更简短的前缀sess_。
    注意,正被包含进来的文件是通过一个Drupal变量定义的。这意味着你完全可以实现自己的会话处理,并将其插入进来以代替Drupal的默认会话处理。例如,memcache模块(drupal.org/project/memcache)实现了函数'sess_open'、 'sess_close'、 'sess_read'、 'sess_write'、'sess_destroy_sid'、 'sess_gc'。将Drupal变量session_inc设置为这个会话代码,来代替使用默认的会话处理:
 
<?php
    variable_set('session_inc',         './sites/all/modules/memcache/memcache-session.inc');                  
?>
 
    通过在你的settings.php文件中设置该变量,你也可以实现对它的覆写:
 
$conf = array(
    'session_inc' => './sites/all/modules/memcache/memcache-session.inc,
    ...
);

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 cookie的必要性

老葛的Drupal培训班 Think in Drupal

如果浏览器不接受cookie的话,那么就建立不了会话。这是因为PHP指令sessions_use_only_cookies已被设置为1;而sessions.use_trans_sid也被设置为0,这样其替代选项(在URL的查询字符串中传递PHPSESSID)也被禁用。这种方式是Zend所推荐的:
 
    基于URL的会话管理与基于cookie的相比,安全性更差一些。例如,用户可能向朋友发送一封email,其中带有了一个URL,它包含了处于激活状态的会话ID,或者用户可能将一个包含了会话ID的URL,保存在他们的书签里,之后再使用同一会话ID来访问你的站点。
 
    当PHPSESSID出现在一个站点的查询字符串中时,它标志着主机提供商已经锁住了PHP,并且不允许通过ini_set()函数在运行时设置PHP指令。可选的一种替代方式是将设置移到.htaccess文件中(如果在主机上,PHP是作为Apache的一个模块运行的话)或者一个本地的php.ini文件中(如果在主机上,PHP是作为一个可执行的CGI运行时)。
  为了防止会话欺诈,当用户登录时会重新生成会话ID(参看modules/user/user.module里的user_authenticate_finalize()函数)。当用户修改密码时,也会重新生成会话。

Drupal版本:

Drupal专业开发指南 第16章 存储

 

会话信息存储在表sessions中,在引导指令的DRUPAL_BOOTSTRAP_SESSION阶段,它将会话ID和Drupal用户ID关联了起来(关于Drupal引导指令处理流程的更多详细,请参看第15章)。事实上,在Drupal中广泛使用的$user对象,就是在本阶段使用includes/session.inc中的sess_read()函数初步建立的。($user对象是如何建立的,请参看第6章)。
 
    表16-2给出了存储会话的数据库表结构:
 
16-2. Sessions表的结构
字段       类型  长度  描述
uid         int             认证用户的用户ID(匿名用户为0)
sid         int     64      由PHP生成的会话ID
hostname    varchar 128     这个会话ID最后使用的IP地址
timestamp  int             最后页面请求的Unix时间戳
cache       int             用户最后发布的时间,用于执行最小缓存生命周期
session     text    big     存储在$_SESSION中的序列化数据
 
    当Drupal提供一个页面时,最后一项工作就是将会话写入表sessions中(参看includes/session.inc里面的sess_write())。只有当浏览器提供了一个有效的cookie时,才这样做,这样就阻止了网络爬虫抓取页面时向表sessions中写入大量数据。

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 会话生命周期

 

16-1展示了会话生命周期。当一个浏览器向服务器发送一个请求时,会话开始。在Drupal的引导指令流程(参看includes/bootstrap.inc)的DRUPAL_BOOTSTRAP_SESSION阶段,会话代码开始运行。如果浏览器没有出示一个已有的cookie,那么PHP的会话管理系统将为浏览器分配一个新cookie,里面包含了一个新的PHP会话ID。这个ID通常是一个32位的唯一的MD5哈希字符串。当然,PHP5允许你将配置指令session.hash_function设置为1,这样你就可以使用SHA-1哈希,来得到40位的字符串作为会话ID。
 
注意 MD5是一个计算文本字符串哈希值的算法,也是Drupal中选用的哈希算法。关于MD5和其它哈希算法的更多信息,请参看http://en.wikipedia.org/wiki/Cryptographic_hash_functions
 
    Drupal接着以该会话ID为键,来检查在表sessions中是否存在相应的记录。如果存在的话,那么includes/sessions.inc中的函数sess_read()将取回会话数据,并进行一次SQL JOIN操作,对sessions表中的记录与users表中的对应记录进行联合。这一联合的结果是得到了一个对象,它包含了两条记录的所有字段和值。这就是$user对象,在Drupal中经常使用的一个全局变量(参看第6章)。那么,通过查看$user对象,具体一点就是$user->session, $user->sid, $user->hostname, $user->timestamp,和$user->cache,也可以访问会话数据。在sess_read()中,当前用户的角色也被查找出来并分配给$user->roles。
    对于会话中的用户ID,如果在users表中,没有一个对应的用户ID与之匹配,那么会发生什么呢?这个问题具有欺骗性。由于Drupal的安装器在users表中创建了一个用户ID为0的记录,而且匿名用户在sessions表中的uid被指定为0,所以联合总是成功的。
 
警告 对于你的Drupal安装,永远不要删除users表中的所有记录。因为,为了让Drupal正常工作,该表中必须包含一条用户ID为0的记录。
 
    如果你想找出用户访问一个页面的最后时间,你可以使用$user->timestamp(记住,它来自于sessions表),或者使用$user->access,后者保存在users表中。对于这两者来说,$user->timestamp如果存在的话,那么它会更精确一点,这是因为$user->access的更新受限于节流,默认情况下180秒才更新一次。通过设置Drupal变量session_write_interval,可以修改这个值。这里是includes/session.inc中sess_write()函数的代码片断:
 
// Last access time is updated no more frequently than once every 180 seconds.
// This reduces contention in the users table.
$session_write_interval = variable_get('session_write_interval', 180);
if ($user->uid && time() - $user->access > $session_write_interval) {
    db_query("UPDATE {users} SET access = %d WHERE uid = %d", time(), $user->uid);
}
 
    当然,对于初次访问的用户,因为还没有保存时间戳,所以$user->timestamp和$user->access都不可用。
    当页面已被发送给浏览器时,最后一步工作就是关闭会话。PHP触发includes/session.inc里面的sess_write()函数,该函数将$_SESSION(请求期间的)中存放的所有东西都写入到sessions表中。只有当你确实需要时,还有最好能够确定该用户已被认证,这样才将数据存储在$_SESSION中。这样在网络爬虫抓取页面时,就不会向sessions表中写入数据了,而该表的大小能够直接影响到性能。
 
16-1 Drupal是如何使用会话来实例化$user对象的

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 会话对话模仿

 

下面是一些虚拟的例子,模拟了在你的浏览器访问Drupal站点时,从会话的角度来看,都发生了什么。
 
初次访问
浏览器:嗨,请给我一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:对不起,我没有;我这是第一次来这里。
Drupal:好吧,给你一个。
 
第二次访问
浏览器:请再给我一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:好的,它在这里。它的会话号码为6tc47s8jd6rls9cugkdrrjm8h5。
Drupal:嗯,不过在我的记录中找不到它。但是还是给你页面。我将会把你的会话ID记录下来,以备你再次访问。
 
 
拥有帐号的用户
(当用户创建了一个帐号并点击了登录按钮时。)
浏览器:嗨!我需要一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:好的,它在这里。它的会话号码为31bfa29408ebb23239042ca8f0f77652
Drupal:你好,乔!(喃喃而语)你的用户ID为384,你喜欢将你的评论嵌套起来,你的咖啡黑色的。给你一个新的cookie,这样你的会话就不会被盗用。我将记录你的这次访问。玩的愉快。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 常见任务

 

下面是一些你使用会话或者修改会话设置的常见方式。
 
修改cookie的存活周期
    包含会话ID的cookie的存活周期,是由settings.php中的session.cookie_lifetime控制的,其默认值为2,000,000秒(大约23天)。如果将该值设为0,那么当用户关闭浏览器时,cookie将被立即销毁。
 
修改会话名字
    当将网站部署在多个子域名上面时,就会遇到一个关于会话的常见问题。由于每个站点为session.cookie_domain使用相同的默认值,默认情况下PHPSESSID的session.name也相同,用户发现他们在任何给定时间只能登录到一个站点上。通过为每个站点创建一个唯一的会话名字,Drupal就可以解决这个问题。这里作了一些修改,会话名字是基于网站基URL的MD5哈希。
 
    通过删除settings.php中的一行代码前面的注释符,并声明$cookie_domain变量的值,就可以绕过自动生成会话名字了。该变量的值只能包含字母数字字符。下面是settings.php中对应的部分:
 
/**
 * Drupal automatically generates a unique session cookie name for each site
 * based on on its full domain name. If you have multiple domains pointing at
 * the same Drupal site, you can either redirect them all to a single domain
 * (see comment in .htaccess), or uncomment the line below and specify their
 * shared base domain. Doing so assures that users remain logged in as they
 * cross between your various domains.
 */
# $cookie_domain = 'example.com';
 
注意Drupal中,Perl风格的注释符号(#)只用于settings.php,.htaccess, robots.txt文件中,而实际的Perl和shell脚本都放在scripts目录中。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 将数据存储在会话中

 

由于会话系统能将数据自动存储起来,所以将数据存储在用户的会话中,会非常方便。无论是什么时候,你想在用户访问(或者多次访问,使用session.cookie_lifetime)期间为其存储一些数据时,都可以使用全局变量$_SESSION:
 
$_SESSION['favorite_color'] = $favorite_color;
 
    接着,在接下来的请求中,使用下面代码取回该值:
 
$favorite_color = $_SESSION['favorite_color'];
 
    如果你知道用户的uid,并且你想为用户存储一些数据,那么有一个更实用的方法,就是将数据作为一个唯一的属性存放在$user对象中,比如通过调用user_save($user, array('foo' => $bar))就实现了$user->foo =$bar,该方法将数据序列化并保存到users表的data列中。如果信息是暂时的,并且你不介意它的丢失,或者你想为匿名用户存储一些短期数据,那么你也可以将它存放在会话中。如果你想将用户的偏爱与用户永远的绑定在一起,那么可将它存储在$user对象中。
 
警告$user对象不能用来为匿名用户存储信息。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 总结

 

读完本章后,你应该能够:
    理解Drupal是如何修改PHP的会话处理的。
    理解哪些文件包含了会话配置信息。
    理解会话生命周期以及在一个请求期间是如何创建Drupal的$user对象的。
    在用户的会话中存储数据和取回数据

老葛的Drupal培训班 Think in Drupal

Drupal版本: