作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal的成功,离不开社区的各种用户的支持;同时,作为一个搭建社区类网站的软件,Drupal程序本身对用户系统提供了完备的支持。使用Drupal可以方便的搭建一个社交网络式的站点、一个微博类型的站点、一个社区型电子商务站点,等等。在本章节中,我们首先学习Drupal中用户的结构定义,接着学习有关用户的钩子函数和常见API函数。最后我们讲解有关Drupal用户统一登录的相关技术。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
登录用户,必须启用cookie;匿名用户则不需要cookie。一个关闭了cookie的用户,仍然可以以匿名的身份与Drupal进行交互。
在引导指令流程的会话阶段,Drupal创建了一个全局$user对象,用来作为当前用户的标识。如果用户没有登录(这样就没有建立一个会话cookie),那么它将被当作匿名用户对待。创建匿名用户的代码如下所示(位于bootstrap.inc):
function drupal_anonymous_user() {
$user = new stdClass();
$user->uid = 0;
$user->hostname = ip_address();
$user->roles = array();
$user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
$user->cache = 0;
return $user;
}
另一方面,如果用户当前登录了,那么可以通过使用用户的ID,通过关联表users和sessions,来创建对象$user。两个表中的所有字段都放到了对象$user中。
注意:用户的ID是在用户注册时或者管理员创建用户时所分配的一个序列数。这个ID是users表中的主键。
我们可以向主题中的page.tpl.php添加以下代码:
<?php debug($user); ?>
这样我们就可以很容易的查看$user对象的内部结构了。下面是用户1的$user对象的一个实例结构:
stdClass::__set_state(array(
'uid' => '1',
'name' => 'admin',
'pass' => '$S$Cv6KBmupqlDMHO/yQ4GadB.JGaRlASPQ3rtJ1SS0f.GiSvJO5BRM',
'mail' => 'g089h515r806@gmail.com',
'theme' => '',
'signature' => '',
'signature_format' => NULL,
'created' => '1297216111',
'access' => '1310787362',
'login' => '1308985143',
'status' => '1',
'timezone' => NULL,
'language' => '',
'picture' => '0',
'init' => 'g089h515r806@gmail.com',
'data' => false,
'sid' => 'Bjfjz8wSsOkQ1d7BeYfhzmerrpVfR2VM3MRjQuuciKU',
'ssid' => '',
'hostname' => '127.0.0.1',
'timestamp' => '1310787484',
'cache' => '0',
'session' => 'batches|a:1:{i:2;b:1;}updates_remaining|a:0:{}',
'roles' =>
array (
2 => 'authenticated user',
3 => 'administrator',
),
))
在上面显示的$user对象中,斜体字段意味着数据来自于sessions表。表6-1解释了$user对象的组成部分:
表6-1 $user的组成部分
组成 描述
来自于表users
uid 用户ID。它是表users的主键,在Drupal系统中是唯一的。
name 用户的用户名,在用户登录时使用。
pass 用户的SHA哈希密码,在用户登录时进行对比。由于没有保存用户的原始明文密码,所以密码只能被重置,不能被恢复。
mail 用户当前的email地址。
theme 用户的默认主题。
signature 用户在他/她的账号页面所输入的签名。只有当评论模块(comment module)启用时,当用户添加一个评论时,才会看到签名。
signature_format 签名的文本格式。
created 用户账号创建时的Unix时间戳。
access 用户最近一次访问的Unix时间戳。
login 用户最近一次成功登录的Unix时间戳。
status 1表示正常激活的用户,0表示被拒绝访问的用户。
timezone 用户时区与GMT之间的差异,以秒为单位。
language 用户的默认语言。只有在站点上启用了多语言,并且用户通过编辑账号选择了一个优先语言时,才不为空。
picture 用户头像图片的文件ID。
init 用户注册时提供的初始email地址。
data 这个字段在Drupal7中,不建议使用。请使用Field API来保存需要存储的数据。
来自于表user_roles
roles 分配给当前用户的角色。
来自于表session
sid 通过PHP分配给当前用户会话的会话ID。
Ssid 安全会话ID。
hostname 用户浏览当前页面时所使用的IP地址。
timestamp 一个Unix时间戳,表示用户的浏览器最近接收一个完整页面的时间。
cache 一个用于per-user caching(参看 includes/cache.inc)的时间戳
session $_SESSION的序列化内容。在用户会话期间,模块可以向这里存储任意数据。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
实际情况不同,解决Drupal与其它站点、系统之间的用户同步的方式也不尽相同。Drupal有多个第三方模块,用来解决这样的问题。我们对常用的这些第三方模块加以介绍,方便大家根据自己的实际情况,选择对应的解决方案。
Single sign on(单点登陆)模块
• 项目地址:http://drupal.org/project/sso
• 适用情景:同一台服务器,多个Drupal安装,用户共享,域名不同
• 实现机制:通过建立子站点与主站点之间的会话信息的对应关系,重新实现Drupal的会话机制,来实现单点登陆
Bakery单点登录系统
• 项目地址:http://drupal.org/project/bakery
• 适用情景:Drupal.org官方网站的解决方案,不同的服务器,主域名相同,子域名不同,比如example.com、news.example.com、forum.example.com。
• 实现机制:由于同一个主域名下,cookie可以共享,通过站点之间调整并分发cookie实现。
CAS
• 项目地址: http://drupal.org/project/cas
• 适用情景: JAVA CAS所适用的各种场景,适用范围广。可用于多种情况。
• 实现机制:利用phpCAS库函数,将Drupal与Java的CAS系统相集成,实现统一用户登录。
•
LDAP integration
• 项目地址:http://drupal.org/project/ldap_integration
• 适用情景:符合轻量级目录访问协议的各种情景
• 实现机制:将Drupal与LDAP服务器相集成,利用LDAP服务器解决用户登录。它包含三个子模块ldapauth、ldapgroups、ldapdata。ldapauth可以针对多个LDAP服务器进行认证;ldapgroups可以把ldap group处理为Drupal角色;ldapdata为Drupal访问ldap数据提供读写权限
Services 模块
• 项目地址:http://drupal.org/project/services
• 适用情景:从其它系统访问Drupal中的用户
• 实现机制:Services 模块,对用户进行了封装,对外提供了webservice服务,供其它系统调用。它提供了多种接口方式,比如XMLRPC、 JSON、JSON-RPC、 REST、 SOAP、 AMF等等。Services 模块不是专门用于用户登录的,我们只是借助于它将Drupal的用户信息封装成web service服务。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在前面列举了多种解决方案,但是在实践中,当我们用到统一用户登录的时候,我们可以首先考虑一下,Drupal内置的统一用户登录是否适用。是的,Drupal自带了一种统一用户登录解决方案。
我们首先来看一下,内置的解决方案适用的条件,它包含三点:
• 这些多个站点的域名必须一致,比如 www.example.com、 forums.example.com 、subsite.example.com
• 必须使用 MySQL.
• 这些站点必须在同一服务器上.
这种内置的统一用户登录机制,充分利用了Drupal的数据库表前缀机制,Drupal本身的跨表查询的机制、MySQL的跨数据库查询能力,还有通过设置cookie domain可以共享多个站点的session会话信息。
我们现在来实际操作一下,假定我们的主站点为thinkindrupal.com,同时还包含两个子站点answer.thinkindrupal.com、forum.thinkindrupal.com。我们要在本地搭建环境,所以首先我们需要修改我们的hosts文件,我这里是vista操作系统,使用管理员身份打开hosts文件,然后向里面添加以下信息:
127.0.0.1 thinkindrupal.com
127.0.0.1 answer.thinkindrupal.com
127.0.0.1 forum.thinkindrupal.com
接着,我们在一个空的web根目录,在xampp环境下就是htdocs目录,放置一个Drupal安装,然后在sites目录下,创建3个文件夹,thinkindrupal.com、answer.thinkindrupal.com、forum.thinkindrupal.com。目录结构如图所示:
接着,我们分别按照正常方式安装thinkindrupal.com、answer.thinkindrupal.com、forum.thinkindrupal.com三个站点。这里假定它们对应的数据库分别为tid、tid_answer、tid_forum。没有的话,我们分别为其创建好就可以了。
下面是没有共享用户时的settings.php文件中的数据库配置:
thinkindrupal.com :
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
answer.thinkindrupal.com:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_answer',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
forum.thinkindrupal.com
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_forum',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
在安装Drupal时,我们分别将三个站点用户1的用户名设置为了admin、answer、forum。为了让answer、forum两个子站点能够共享主站的用户信息,让我们分别修改它们的settings.php文件,修改后的配置如下所示:
answer.thinkindrupal.com:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_answer',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => array(
'default' => '',
'users' => 'tid.',
'sessions' => 'tid.',
'role' => 'tid.',
'authmap' => 'tid.',
),
);
forum.thinkindrupal.com
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_forum',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => array(
'default' => '',
'users' => 'tid.',
'sessions' => 'tid.',
'role' => 'tid.',
'authmap' => 'tid.',
),
);
注意,我们在这里设置了prefix,将users、sessions、role、authmap表挂在了主站上。对于其它表,则采用本安装所使用的数据库。现在我们就可以使用admin用户来登录answer.thinkindrupal.com、forum.thinkindrupal.com两个站点了。
此时,如果我们登录了三个站点后,比如从forum.thinkindrupal.com上登出,但是我们访问thinkindrupal.com站点时,仍然是登录的状态。现在美中不足的就是,登录和登出不是同步的。为了能够同步的登录和登出,我们还需要设置settings.php文件中$cookie_domain。我们将3个settings.php文件中的$cookie_domain都设置为:
$cookie_domain = '.thinkindrupal.com';
注意最前面的“.”号是必须要有的。此时当我们从thinkindrupal.com登录后,访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,仍然处于登录状态。如果我们从thinkindrupal.com登出,那么访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,状态就变成了匿名用户。
初次配起来,可能稍微有点复杂,但它却能解决我们的大问题,这就是我们想要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习:
我们了解了$user对象的组成结构
了解与用户相关的钩子函数
能够熟悉使用常见的用户钩子函数
了解统一用户登录,能够与异构系统整合用户
了解统一用户登录的常见解决方案
熟悉内置的单点登录
此时,如果我们登录了三个站点后,比如从forum.thinkindrupal.com上登出,但是我们访问thinkindrupal.com站点时,仍然是登录的状态。现在美中不足的就是,登录和登出不是同步的。为了能够同步的登录和登出,我们还需要设置settings.php文件中$cookie_domain。我们将3个settings.php文件中的$cookie_domain都设置为:
$cookie_domain = '.thinkindrupal.com';
注意最前面的“.”号是必须要有的。此时当我们从thinkindrupal.com登录后,访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,仍然处于登录状态。如果我们从thinkindrupal.com登出,那么访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,状态就变成了匿名用户。
初次配起来,可能稍微有点复杂,但它却能解决我们的大问题,这就是我们想要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在请求期间,测试用户是否登录的标准方式,是检查$user->uid是否为0。Drupal有个名为user_is_logged_in()的函数可用来检查登录用户(还有一个相应的user_is_anonymous()函数用来检查匿名用户):
if (user_is_logged_in()) {
$output = t('User is logged in.');
else {
$output = t('User is an anonymous user.');
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在Drupal对用户进行各种相关操作时,为其它模块提供了相应的钩子函数,方便与Drupal系统交互。我们来看看Drupal中用户模块都提供了哪些钩子函数,这些信息可参看user.api.php文件。
名字 |
描述 |
取消用户帐户时的钩子函数。 |
|
修改取消用户帐户方法。 |
|
获取一列用户设置或者资料信息类别。 |
|
删除用户时的钩子函数。 |
|
创建用户帐户时的钩子函数。 |
|
在从数据库中加载用户对象时的钩子函数。 |
|
用户刚登录时的钩子函数。 |
|
用户刚登出时的钩子函数。 |
|
添加用户批量操作。 |
|
用户帐户将被创建或者将被更新时的钩子函数。 |
|
删除用户角色时,调用的钩子函数。 |
|
添加用户角色时,调用的钩子函数。 |
|
将要保存用户角色时,调用的钩子函数。 |
|
用户角色已被更新时,调用的钩子函数。 |
|
用户帐户已被更新时,调用的钩子函数。 |
|
用户帐户信息正被显示时,调用的钩子函数。 |
|
用户已构建;用户使用这个钩子可以修改其结构化的内容。 |
此外,系统模块还提供了一个与用户相关的钩子函数,这就是hook_username_alter(&$name, $account),它是用来修改用户名的,这句话的具体含义,我们在后面通过实例来讲解。
我们从用户的钩子函数中,选择较为通用的与增删改查相关的钩子,比如delete、insert、load、presave、update、view、view_alter。我们检查Drupal系统中所有的钩子函数,把与之相关的加以归纳,总结出如下所示的表。
delete |
insert |
load |
presave |
update |
view |
view_alter |
|
comment |
X |
X |
X |
X |
X |
X |
X |
entity |
X |
X |
X |
X |
X |
X |
X |
field |
X |
X |
X |
X |
X |
||
file |
X |
X |
X |
X |
X |
||
filter_format |
X |
X |
X |
||||
image_style |
X |
||||||
menu |
X |
X |
X |
||||
menu_link |
X |
X |
X |
||||
node |
X |
X |
X |
X |
X |
X |
X |
node_type |
X |
X |
X |
||||
path |
X |
X |
X |
||||
taxonomy_term |
X |
X |
X |
X |
X |
X |
|
taxonomy_vocabulary |
X |
X |
X |
X |
X |
||
user |
X |
X |
X |
X |
X |
X |
X |
user_role |
X |
X |
X |
X |
最左边的一列,表示Drupal中的各种对象,最上面的一行,是与增删改查相关的钩子,格子中的内容表示对应钩子是否存在,X表示存在,空白则表示不存在。我们可以看到,comment、entity、node、taxonomy_term、user最为接近,可以划分为一组;field、file、taxonomy_vocabulary、user_role比较接近;而filter_format、menu、menu_link、node_type、path,主要实现了delete、insert、update,也可以划分为一组。
在Drupal中,User本身是实体的一个实现,我们在后面的学习中,将会陆续的看到,评论、分类术语、节点也都属于实体。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们来看一个来自于实践的实例,有这样的一个网站需求,它用来展示一个中学各个班级的相关信息。注册用户为班级的班主任,每个班主任可以创建一个班级。在这里,班主任就是Drupal中的用户,班级就可以处理成为Drupal中的节点,一个用户只可以创建一个班级节点,这有相应的第三方模块可以控制,我们在这里不讨论这个。在这里,我们关注一个与我们的用户系统相关的需求,当班主任注册自己的帐户时,他希望能够同时输入班级的名字,在创建自己的帐户时,同时创建了班级节点;当班主任登录时,检查它所填写的班级信息的完整性,如果部分信息没有填完,将其重定向到班级的编辑页面,并提示他完善班级信息。
为此,我们首先需要创建一个班级节点类型。导航到admin/structure/types,点击“添加内容类型”链接,在这里我们输入与班级相关的信息:
注意这里面,机读名字的输入,它有点小技巧,首先在名称里面输入拼音,然后点击右边的编辑链接,然后再把名称里面的拼音改为中文。这是由于Drupal对中文的支持不大友好,不过这并不影响使用。
除非我们把系统中的注册用户用作班主任,否则我们还需要创建一个“班主任”用户角色,导航到admin/people/permissions/roles,在这里我们添加一个“班主任”角色。接着编辑班主任的权限,授予它创建编辑班级节点的权限,也就是“班级: 创建新内容”、“班级: 编辑自己内容”。
这样我们就完成了前期的准备工作。让我们来创建这个模块,不妨把我们的模块命名为“class_teacher”,中文名字就叫作“班主任”。和前面一样,我们先准备好文件夹class_teacher,然后向里面添加class_teacher.info、 class_teacher.module文件。
我们向info文件中,添加以下内容,注意文件本身的编码格式:
name = 班主任
description = 在注册班主任用户时,为其添加一个班级节点
core = 7.x
接着,我们向module文件中添加实际的逻辑代码,我们首先实现在用户注册表单上显示一个文本框字段“班级名”,以供用户注册时输入使用。在里面输出以下代码:
<?php
/**
* @file
* 在注册班主任用户时,为其添加一个班级节点.
*/
/**
* 实现钩子hook_form_FORM_ID_alter().
*/
function class_teacher_form_user_register_form_alter(&$form, &$form_state) { $form['class_name'] = array(
'#type' => 'textfield',
'#title' => t('班级名'),
'#maxlength' => 255,
'#description' => t('请输出您所管理的班级名.') ,
'#weight' => 1,
);
$form['#submit'][] = 'class_teacher_user_register_submit';
}
在这里面,我们再次使用了hook_form_FORM_ID_alter钩子函数,注意这里面用户注册所用的表单ID为user_register_form。在上面的代码中,我们为注册表单添加了一个表单元素“班级名”,同时添加了一个提交处理函数。
这样,当我们启用了这个模块后,换一个浏览器,使用匿名用户访问注册页面,此时我们就可以看到我们新加的“班级名”字段了。如图所示:
接下来,我们添加处理函数的相关代码:
/**
* 为注册表单新增的一个处理函数,用来处理班级名和其它.
*/
function class_teacher_user_register_submit($form, &$form_state){
//全局用户,也就是当前新注册用户
global $user;
//班级名的值
$class_name = $form_state['values']['class_name'];
//新建一个节点对象
$node = new stdClass();
//为节点对象赋值
$node->title = $class_name;
$node->uid = $user->uid;
$node->type = 'banji';
//保存节点对象
node_save($node);
//将用户的角色设置为班主任,这里班主任的role id为4
$roles = array(
2 => 'authenticated user',
4 => '班主任',
);
//保存用户对象
user_save($user,array('roles' => $roles));
}
这段代码有详细的注释,它的作用就是保存班级节点,设置新建用户的角色。注意,如果我们将设置用户角色的代码修改为:
//将用户的角色设置为班主任,这里班主任的role id为4
$user->roles = array(
2 => 'authenticated user',
4 => '班主任',
);
//保存用户对象
user_save($user);
此时我们并不能有效地设置用户的角色。这里需要注意一下,这段不工作的代码,是很多时候第一时间所想到的,我最初也这样写。这应该和user_save本身的机制有关系。我们在这里不做更深的研究,有兴趣的读者可以自己研究一下,为什么不工作。
如果你一直在使用本教程里面的模块进行练习,并且在同一站点,那么我们此时需要禁用我们在菜单系统一章用到个人中心模块“my”,这个模块对于改造用户的个人主页,非常有帮助的,但是在这里,与我们的这个模块存在冲突。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们现在想要在用户的主页显示班级信息,这样用户登录进来就能看到自己的班级。向module文件中添加以下代码:
/**
* 实现钩子hook_user_view().
*/
function class_teacher_user_view($account, $view_mode, $langcode){
//如果用户具有创建班级节点的权限,在这里也就是用户的角色为班主任
if (user_access('create banji content', $account)) {
//drupal_set_message('123456');
//首先是读取用户创建的班级节点。
$node = db_select('node', 'n')
->fields('n')
->condition('n.uid', $account->uid)
->condition('n.type','banji')
->execute()
->fetchObject();
//根据节点是否存在,为$markup赋值。
$markup = "";
if(!empty($node)){
$markup = l($node->title, "node/$node->nid", array('attributes' => array('target' => '_blank')));
}else{
$markup = l(t('添加班级'), "node/add/banji", array('attributes' => array('target' => '_blank')));
}
//添加有关班级的信息,注意这里的信息类型为user_profile_item
$account->content['summary']['banji'] = array(
'#type' => 'user_profile_item',
'#title' => t('班级'),
'#markup' => $markup,
'#attributes' => array('class' => array('banji')),
);
}
}
上面的代码中,有详细的中文注释,这里就不再过多解释了,需要注意的是'create banji content'这个权限,我是通过查询数据库最后才确认的,中间是内容类型的机读名字。我们向用户帐户的内容里面,添加了有关班级的信息。这里还需要注意的是信息的结构是一个关联数组,我们这里用到了它的四个键'#type'、'#title'、'#markup'、'#attributes'。user_profile_item类型的输出,由modules\user下面的user-profile-item.tpl.php模板文件负责。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下,当用户登录时,检查他是否创建了班级节点,如果没有创建,提示它创建,如果创建了但是信息不完善,提示他完善班级的资料信息。向module文件中添加以下代码:
/**
* 实现钩子hook_user_login().
*/
function class_teacher_user_login(&$edit, $account){
//首先是读取用户创建的班级节点。
$node = db_select('node', 'n')
->fields('n')
->condition('n.uid', $account->uid)
->condition('n.type','banji')
->execute()
->fetchObject();
if(!empty($node)){
//这里假定节点的默认语言为en,我们以body字段为判断依据,可根据实际调整
//debug($node);
$node = node_load($node->nid);
if(empty($node->body['en'][0]['value'])){
drupal_set_message(t('请完善班级信息'));
//重定向到节点的编辑页面
//drupal_goto('node/'.$node->nid.'/edit');
$url = url('node/'.$node->nid.'/edit');
header('Location: ' . $url);
exit();
}
}else{
drupal_set_message(t('请添加班级信息'));
//重定向到节点的创建页面
//drupal_goto('node/add/banji');
$url = url('node/add/banji');
header('Location: ' . $url);
exit();
}
}
在上面的代码中,我最初使用drupal_goto来进行重定向,在这种情况下,在用户登录页面登录时,可以正常工作,但是如果使用了登录表单区块,重定向就存在问题了,此时将其修改为了原始的header('Location: ' . $url);形式,这样在两种情况下都能正常工作。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
最后,让我们在这个模块中,再增加一个需求,那就是为用户增加一个真实姓名,当显示用户名字时,我们使用真实姓名来代替默认的username。
我们导航到admin/config/people/accounts,点击“管理字段”,进入页面,我们添加新的字段“真实姓名”。
接着点击保存按钮,在接下来的两个表单中,我们不做任何修改,直接保存即可。在Drupal7中,尽可能的使用字段的形式来扩充用户对象,而不是使用已经废弃的profile模块。Drupal7核心自带的profile模块仅仅是用来升级Drupal6,除此以外,再无别的用处,这个模块将在Drupal8中从核心中移出去。
完成这一准备工作以后,让我们向module文件中添加以下代码:
/**
* 实现钩子hook_username_alter().
*/
function class_teacher_username_alter(&$name, $account){
//debug($account);
$user = user_load($account->uid);
// 使用用户的真实姓名来代替默认的username.
if (isset($user->field_real_name['und'][0]['value'])) {
$name = $user->field_real_name['und'][0]['value'];
}
}
这样当我们访问我们的个人主页时,username就被替换为了现在的真实姓名,如图所示:
现在,对于钩子函数hook_username_alter(),我们就能够更好的理解它的用途了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果我们的站点非常简单,那么使用Drupal自带的用户系统就可以了。对于稍微复杂一点的站点,比如我们的站点使用Discuz作为论坛,此时就有了Drupal与Discuz用户集成的需求。除了与常见的Discuz集成以外,在实际中,还存在与.net系统,Java网站系统的用户集成。Drupal的用户系统,为我们提供了这样的机制,能够方便的与各种异构系统相集成。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们首先来看一个简单的实例。如果一个单位使用了Drupal6搭建了自己的网站,觉得比较好用,在另一个新建网站上使用了Drupal7,然后提出了这样的需求,那就是Drupal6站点的用户能够直接登录到Drupal7上,这里假定两个网站都放在同一个服务器上。
我们把这个模块的名字命名为d6user,表示Drupal 6的用户集成,先创建两个文件d6user.info, d6user.module。接着我们向info文件中添加以下代码:
name = Drupal 6 User
description = Drupal 6 老网站用户的同步登录.
core = 7.x
注意文件的编码格式,UTF-8。接着我们向module文件中添加逻辑代码:
<?php
/**
* @file
* 当登录Drupal7站点时,检查是否在drupal6站点存在这个帐户,
* 如果存在,将其保存到新的站点.
*/
/**
* 实现钩子hook_form_FORMID_alter().
*/
function d6user_form_alter(&$form, &$form_state, $form_id){
//这里我们对于表单user_login,user_login_block同时添加了一个验证器
if($form_id == 'user_login' || $form_id == 'user_login_block'){
$form['#validate'] = d6user_login_default_validators();
}
}
我们需要在用户登录时,提前检查Drupal6站点是否存在该用户,所以我们需要修改user_login、user_login_block表单,把它的验证函数集合,替换成我们自己的。我们先来看一下Drupal核心为登录定义的验证器函数:
function user_login_default_validators() {
return array('user_login_name_validate', 'user_login_authenticate_validate', 'user_login_final_validate');
}
我们看到,这里定义了3个验证器函数:
user_login_name_validate:用来检查是否设置了用户名,并且它对应用户的状态是否为0,如果属于这种情况,则为表单返回错误信息;
user_login_authenticate_validate:首先会检查连续尝试登录未成功的次数,如果超过了最大次数,则返回;反之,会根据用户名、密码、以及状态为1,来对users表进行查询,如果返回结果的不为空,为user_login_name_validate设置对应的标记$form_state['uid'];
user_login_final_validate:会根据$form_state['uid']是否存在进行判断,如果不存在,则为表单返回错误信息,反之登录用户。
对于大多数的外部验证系统,只需要修改第二个验证器就可以了。在我们的这个例子当中,我们将三个验证器全部保留了下来,并在第一个验证器函数后面追加我们自定义的验证器函数。代码如下:
/**
* 我们自己的登录验证函数集合,.
*/
function d6user_login_default_validators(){
//注意这里面保留了Drupal自带的验证器,只是在中间加上了一个自定义的验证器。这些验证器的执行存在先后顺序的
return array('user_login_name_validate', 'd6user_user_form_validate', 'user_login_authenticate_validate', 'user_login_final_validate');
}
最后,我们看一下d6user_user_form_validate函数的代码:
/**
* 我们自己定义的登录验证函数,它在user_login_authenticate_validate前面执行.
*/
function d6user_user_form_validate($form, &$form_state){
//form_set_error('name',t('用户名,密码不匹配.')); 这里保留了调试信息
$name = $form_state['values']['name'];
$pass = $form_state['values']['pass'];
if (!empty($name) && !empty($pass)) {
//drupal_set_message('123');
// 这里保留了调试信息,用户调试代码,方便大家测试
$account = db_query("SELECT * FROM {users} WHERE name = :name ", array(':name' => $name))->fetchObject();
//如果用户名存在,则返回.
if ($account) {
return;
}else{
// $sql = "SELECT * FROM {users} WHERE name = :name AND pass = :pass";
//drupal_set_message('123456');
//我们向Drupal6站点的用户表进行查询,检查该用户是否存在。
db_set_active('d6user');
$account = db_query("SELECT * FROM users WHERE name = :name AND pass = :pass", array(':name' => $name, ':pass' => md5($pass)))->fetchObject();
db_set_active('default');
if($account){
// drupal_set_message('1234567');
//此时,用户帐号在Drupal6中存在,并且用户名密码正确
$userinfo = array(
'name' => $name,
'pass' => $pass,
'mail' => $account->mail,
'init' => $name,
'status' => 1,
'access' => REQUEST_TIME
);
//我们将查询到的信息保存到Drupal7的用户表中
$account = user_save(drupal_anonymous_user(), $userinfo);
}
}
}
}
上述代码中,有详细的注释,这里就不再过多解释。我们需要做的是,修改settings.php文件,让Drupal7站点能够直接访问Drupal6站点的数据库。有关Drupal连接多个数据库,可参看数据库API一章。修改的代码如下:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'thinkindrupal',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',)
;
$databases['d6user']['default'] = array(
'driver' => 'mysql',
'database' => 'drupal6',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
这样,当用户登录时,如果该用户名在Drupal7站点上不存在,那么我们会检查它是否存在于Drupal6站点上,如果存在,自动将其保存到Drupal7的系统中去。