138. 注册与注销
用户user模块是系统最重要最基础的模块之一,同时也是对各种基础组件的综合运用,涉及到的基础有:配置、实体、表单、会话、约束验证、插件、批处理等等,读者需要先综合掌握这些才能深度理解用户模块,在本系列前面已经介绍过的可以算是用户模块的专门主题有:
《权限系统》上下集
《用户角色实体》
《临时储存与消息服务》
推荐先阅读,作为对用户模块的完整性补充,本篇将介绍注册和注销,以及在下一篇中将介绍登录、退出和重置密码。在开始前我们先回顾整理下重要知识点。
用户实体:
关于实体概念请查阅本系列相关主题,Drupal用户系统完全采用实体机制,用户类型实体是一个内容实体,非版本化,可翻译,无bundle,除默认字段外可自由扩展定义更多字段,字段管理见:
/admin/config/people/accounts/fields
(在云客看来能设置bundle更好,这样可以控制不同类型的用户有不同的字段信息)
用户实体类:\Drupal\user\Entity\User
实现接口:\Drupal\user\UserInterface
该接口除继承实体相关接口外,还继承了用户账户接口:
\Drupal\Core\Session\AccountInterface
这使得用户实体对象可以像账户对象一样被注入当前账户对象服务中,被当做账户对象直接使用
储存处理器:Drupal\user\UserStorage
访问控制处理器:Drupal\user\UserAccessControlHandler
用户(实体)数据结构:
用户实体数据库表结构:
实体基本表:users(按标准应该是user,但很多数据库user是保留词,因此释文中指定为复数形式)
实体数据表:users_field_data
角色字段专用表:user__roles
头像字段专用表:user__user_picture
没有版本表及版本数据表
注意数据库表“users_data”不属于用户实体的表,其是供“用户数据服务”用来储存数据的表
默认字段结构:
uid:用户id,无符号整数,这意味着可以储存的用户总量为4294967295(42亿)
uuid:通用唯一识别id
langcode:用户数据本身的语言代码
preferred_langcode:用户首选使用的语言代码,用于浏览站点、首发邮件等
preferred_admin_langcode:当查看管理界面时用户使用的首选语言代码
name:用户名,最大长度60字节
pass:密码,已经过哈希转化,字段类型“password”
mail:用户邮件
timezone:用户时区,php时区标识符
status:用户状态,布尔值,1为启用,0为阻塞(禁用)
created:用户创建时间(注册时间),时间戳
changed:用户实体改变时间(用户数据最后编辑时间)
access:用户最后访问站点的时间,时间戳,在“onKernelTerminate”事件中进行记录
login:用户最后一次登录的时间,时间戳
init:储存注册账户时使用的初始邮件地址
roles:实体引用字段,引用到角色实体
user_picture:用户头像,“image”字段类型
用户模块提供的权限:
administer permissions
管理用户权限的权限,分配权限的权限,可算是超级权限
administer account settings
管理账户设置的权限,如注册方式、发送给用户的邮件内容,可访问该表单
administer users
管理用户的权限,如创建或删除用户、编辑用户信息、改变邮件或密码、启禁用用户。
access user profiles
查看用户信息的权限
change own username
能改变自己的用户名的权限
select account cancellation method
取消账户时,能选择取消方式的权限
cancel account
取消自己账户的权限
用户设置配置(user.settings):
配置对象名“user.settings”,其结构及含义如下:
anonymous: Anonymous #匿名用户的名称或称呼
verify_mail: true #用户自行注册时是否需要邮件确认,此时注册表单将暂不输入密码
notify: #各种用户相关的邮件是否发送
cancel_confirm: true
password_reset: true
status_activated: true
status_blocked: false
status_canceled: false
register_admin_created: true
register_no_approval_required: true
register_pending_approval: true
register: visitors #用户注册的方式,可选值有:visitors、admin_only、visitors_admin_approval
cancel_method: user_cancel_block #取消用户时的默认处理方式
password_reset_timeout: 86400 #密码重置链接超时时间,默认一天
password_strength: true #注册时是否启用密码强度指示器
langcode: en #配置数据使用的语言,设置时应该使用英语,然后在翻译界面进行对应英语翻译
账户创建与编辑表单基类:
用户的注册与注销在系统看来核心即是创建与删除用户实体,创建是比较简单的,但删除则需要考虑该账户在系统中留下的数据如何处理,先看一看如何创建与修改。
该基类提供创建、编辑用户实体的表单基础功能,下文的注册与资料修改表单均继承自她,为描述方便,简称账户表单基类,类如下
\Drupal\user\AccountForm
这继承自内容实体表单,关于该表单的流程原理等请参考本系列实体表单主题,这仅列出一些注意点:
你可能已经注意到在表单显示管理中的字段并不对应实体字段,比如:
联系设置字段就不在实体字段中
用户名和密码在表单显示中被绑定在一起
实体中有邮件地址,但管理表单显示中没有,表单中却有
这是因为很多基本字段不是显示可配置的,因此在管理表单显示中默认不会出现,但系统以额外字段(伪字段)的方式提供了一种控制形式,详见本系列额外字段及实体表单显示主题
注册register账户:
在Drupal中注册账户有两种方式:用户自行注册、管理员代为注册
默认用户自行注册账户路由定义如下:
user.register:
path: '/user/register'
defaults:
_entity_form: 'user.register'
_title: 'Create new account'
requirements:
_access_user_register: 'TRUE'
管理员代为注册账户路由定义如下:
user.admin_create:
path: '/admin/people/create'
defaults:
_entity_form: 'user.register'
_title: 'Add user'
requirements:
_entity_create_access: 'user'
这两种注册方式的区别是:
管理员代注册时可以同时指定账户的状态(是否启用)、分配用户角色,可暂不输入邮件等,而用户自行注册默认不能。
这两种注册方式使用了同一个表单,即用户实体注册表单:
Drupal\user\RegisterForm
这继承自前文的账户表单基类,注册区别等逻辑均在该表单中处理
这里对个字段说明如下:
邮件:
注册时当前用户如果有“administer users”权限,那么邮件不是必填的,通常是管理员代注册可免输邮件,但一旦有值就是必填的,在用户自行注册时,默认需要必填;该字段被设置了以下约束:
UserMailUnique:保证邮件地址唯一
UserMailRequired:在没有用户管理权限时,保证邮件必填
ProtectedUserField:在修改时需要原密码
除约束外,如果有值还会调用邮件验证器服务(\Drupal::service('email.validator'))进行有效性验证,这在email元素类型中进行
用户名:
该字段被设置了两个约束:UserName、UserNameUnique,总要求如下:
默认必填,长度小于等于60字节,全站唯一(不允许两个或多个用户有相同用户名);
不能以空格做开始或结束字符,中间可包含空格,但不能有连续两个或以上的空格;
可包含:大小写字母、数字、“@”、加号、下划线“_”、英文点号、撇号“'”、连字符“-”,中文双引号、中文书名号“《》”以及\x{80}-\x{F7}范围内的字符(如普通中文汉字);
不可包含:回车、中英文叹号、中英文问号、中英文逗号、中英文冒号、“#”、“$”、“%”、“^”、“&”、“*”、“(”、“)”、“=”、“””、“\”、“/”、“~”、“[”、“]”、“{”、“}”、“:”、“;”、“<”、“>”、“|”、非可打印字符、软连字符、回车等空白字符;
是否合法具体可通过以下代码测试:
$name = '云客';
if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name)
|| preg_match(
// Non-printable ISO-8859-1 + NBSP
'/[\x{80}-\x{A0}' .
// Soft-hyphen
'\x{AD}' .
// Various space characters
'\x{2000}-\x{200F}' .
// Bidirectional text overrides
'\x{2028}-\x{202F}' .
// Various text hinting characters
'\x{205F}-\x{206F}' .
// Byte order mark
'\x{FEFF}' .
// Full-width latin
'\x{FF01}-\x{FF60}' .
// Replacement characters
'\x{FFF9}-\x{FFFD}' .
// NULL byte and control characters
'\x{0}-\x{1F}]/u',
$name)
) {
echo '非法字符';
} else {
echo '合法字符';
}
注意:系统默认没有设置屏蔽字符,恶意用户可能注册“管理员”、“版主”、“系统账号”等或涉嫌非法的名字,因此在生产站点中我们通常需要进一步设置约束,请参考本系列表单验证主题
密码:
可以是任意字符,在表单中默认限制最大长度为128字节,超出会被自动截取,在密码哈希处理中也要求明文最长不超过512字节,否则将报错;密码字段被设置了ProtectedUserField约束,设置了该约束的字段在有改变时需要验证原密码
自定义注册:
在系统中,用代码注册一个可登录用户是很简单的,如下即可:
$user=['name'=>'yunke','pass'=>'123456','status'=>1];
$userEntity=\Drupal::entityTypeManager()->getStorage("user")->create($user);
$userEntity->save();
仅需要三项信息即可(用户名、密码、启禁用状态,其中又仅用户名是必须的),在站点实际的注册过程中,主要工作是去设计注册表单。
有很多需求需要自定义注册,这里举几个例子以说明:
注册时由用户来选择角色:
比如一些学校系统在注册时要求选择角色,如学生、老师、行政人员等等,不同角色注册后权限不同,因此必须在注册时进行权限分配,这在drupal的默认实现中是不支持的,在默认表单的设计中,有角色字段,但需要有管理权限的权限才可见,这种权限不可能分配给匿名用户,因此注册时用户无法选择角色,此时可通过表单修改钩子去实现,把角色字段暴露给用户,并过滤可让用户选择的角色,使其不能选择超级管理员等角色;也可通过表单修改钩子设置一个选项字段,依据选项字段的信息映射出不同权限的角色,在验证处理器中进行角色注入
用手机号码快捷注册:
在中国,有些站点为了吸引用户注册,在流程上要求极简,比如仅需要填写手机号码和密码即可,甚至密码也不要求填写(以后均通过短信验证码登录),邮件也不是必填的,在手机短信验证正确时即可允许注册,但默认系统要求用户名是必填的,要实现这样的目的可实现修改钩子,在验证处理器中随机生成一个唯一的用户名,通常不会在用户名字段直接使用手机号,首先是出于保密考虑,其次当我们允许用户同时使用用户名、手机号、邮件进行登录时,便于凭据识别
账户资料修改:
账户资料的编辑修改也有两种情况:管理员直接编辑、用户自行编辑
和注册相比所不同的是这两种情况使用相同的路由,定义如下:
路由名:entity.user.edit_form
路径:/user/{user}/edit
权限:_entity_access: user.update
控制器设置:_entity_form:user.default
该路由详见:\Drupal\user\Entity\UserRouteProvider::getRoutes
控制器直接使用了用户实体默认表单user.default,表单类如下:
Drupal\user\ProfileForm
表单id为user_form,该表单也直接继承了前文的账户表单基类,和注册表单大同小异,主要不同是提供了注销按钮(ID为1的维护账户不能注销)。
注销cancel账户:
注销账户也可以叫做取消账户,这个过程不能仅是简单的删除用户实体,还应考虑到要注销的账户发布的各种数据怎么处理,各种足迹或操作如何善后,而用户模块并不知道如何去进行这些操作,因此用户模块以派发钩子(用户取消钩子:user_cancel)的方式去通知各模块,有善后需求的模块须自己去做处理;注销过程需要进行的工作量可能是很庞大的,因此系统以批处理的方式来完成注销(批处理相关知识请参考本系列批处理API主题)。
系统默认提供了以下几种注销方法(每种注销方法对应一个方法标识符,这里以标识符列出):
user_cancel_block:
禁用帐户并保留其发布的所有内容。
user_cancel_block_unpublish:
禁用账户并撤下其发布的所有内容(转为未发布状态)。
user_cancel_reassign:
删除帐号,把帐号所有的内容转到匿名用户下。
user_cancel_delete:
删除账号及其内容,这种取消方式默认仅能被有“administer users”权限的用户使用
如果模块有特殊需求,可以定义更多取消方法,实现修改钩子“user_cancel_methods”即可,该钩子在以下方法中派发:
user_cancel_methods()
钩子接收修改的参数请见该方法,模块添加的方法会在用户取消钩子派发时进行传递,模块通过识别方法标识符就知道需要做些什么;执行用户取消功能的代码被封装在以下函数中:
user_cancel($edit, $uid, $method)
用户取消钩子“user_cancel”即是在该函数中派发,注意如果取消方法是user_cancel_delete(删除用户和数据),那么并不派发用户取消钩子,模块应实现用户实体删除钩子去进行响应(如node_user_predelete),实现用户取消钩子的默认模块有:
comment : comment_user_cancel(...)
history : history_user_cancel(...)
node : node_user_cancel(...)
模块在实现用户取消钩子时须注意以下事项:
1、如果数据量较大应首选以批处理方式去操作(可参见节点模块的实现),但不应去启动批处理;
2、执行过程中需要考虑当前用户可能是管理员(管理员取消)也可能是被取消的用户(自行取消);
3、如果是系统提供的默认取消方法,模块不需要去删除或禁用用户账户,在执行流程中用户模块会处理,但如果是模块自定义的取消方法,在批处理结束时,针对被取消的用户实体,系统默认禁用而不是删除,默认模块(如节点)已实现的钩子也无法响应某个模块自定义的取消方法,因此模块自定义取消方法时,应依据需求考虑是否主动以其他方法名去调用那些默认实现的钩子
在系统权限配置里面可配置用户是否能取消自己的账户,以及取消时是否能选择取消的方法,在系统账户设置管理页可配置默认的取消方法,取消账户可分为用户自行取消、管理员批量取消两种:
用户自行取消:
默认路由如下:
路由名:entity.user.cancel_form
路径:/user/{user}/cancel
控制器设置:_entity_form: user.cancel
权限要求:_entity_access: user.delete
该路由仅在拥有能注销自己账户的权限时才可访问,默认管理员在用户账户编辑页点击取消时也使用该路由,控制器使用了标准的实体表单user.cancel,表单类如下:
\Drupal\user\Form\UserCancelForm
用户自行取消账户时,系统默认通过发送取消确认邮件的方式进行,如果用户资料里面没有电子邮件地址将无法取消,用户邮件收到的取消链接由以下函数生成:
user_cancel_url(UserInterface $account, $options = [])
对应路由“user.cancel_confirm”,控制器如下:
\Drupal\user\Controller\UserController::confirmCancel
如果是管理员进行的取消,将立即执行
管理员批量取消:
管理员可在用户管理页进行批量取消,路由定义如下:
user.multiple_cancel_confirm:
path: '/admin/people/cancel'
defaults:
_form: '\Drupal\user\Form\UserMultipleCancelConfirm'
_title: 'Cancel user'
requirements:
_permission: 'administer users'
注意:
以上两种取消方式仅处理用户接口问题,核心工作均在前文的user_cancel方法中执行;
不论是何种取消方式,一旦被取消后,被取消用户在下一个请求中即被退出登录,如果是在用户自行取消的情况下,在当前请求中(取消请求中)自取消起后续流程将是在匿名用户背景下执行;
自定义取消核心是执行函数:user_cancel($edit, $uid, $method)
由于篇幅有限,本篇完,下篇将接着讲述登录、退出、重置密码
补充:
1、用户注册默认没有频率限制
2、维护账户(uid为1的账户)不能被取消
3、在数据库中储存了匿名用户实体数据,id为0,这在用户模块安装时建立,但需注意系统通常使用的是直接实例化的会话匿名账户对象,其中包含的数据并不来自数据库,数据库储存匿名用户实体的意义在于一致性考虑,这在程序上有时是必要的
4、和用户模块相关的“在线用户块”和“新进用户块”请参考视图主题