139. 登录、退出、重置密码

接上篇《注册与注销》,本篇讲述登录、退出、重置密码,以完成对用户模块的完整性补充

请求认证过程:

在系统运行过程中获取当前账户对象使用以下服务:

服务idcurrent_user

类:Drupal\Core\Session\AccountProxy

这实际上是一个代理对象,在默认情况下被代理的真实使用的账户对象是:

\Drupal\Core\Session\UserSession

这在系统派发请求事件时由认证管理器通过认证提供器实例化并返回注入,默认使用的认证提供器是:

服务iduser.authentication.cookie

类:\Drupal\user\Authentication\Provider\Cookie

在逻辑上是去会话数据中查找变量“uid”,如果是一个有效的uid那么即认证成功,由此可见登录过程即是开启会话并在会话数据中写入uid的过程,换句话说登录即:

$request->getSession()->set('uid', 1);

只要该行代码被系统运行,那么用户就以超级管理员身份登录了

(云客提醒你:不要在系统中运行不可靠来源的模块或代码,如你所见,不需要知道你的账户名和密码,仅一行代码即可完全控制你的网站)

 

默认登录过程:

登录路由定义如下:

user.login:
  path: '/user/login'
  defaults:
    _form: '\Drupal\user\Form\UserLoginForm'
    _title: 'Log in'
  requirements:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE

该路由在登录状态下无法访问,未登录时在维护模式下可访问,登录由以下表单完成:

\Drupal\user\Form\UserLoginForm

作为默认的登录表单,她完整的向开发者演示了应如何登录,核心逻辑有以下几部分:

一、首先检查用户是否处于阻塞状态

二、其次为了避免密码猜测,必须进行频率限制,关于频率限制详见本系列《洪水控制防护flood》主题,在频率限制方面既限制ip也限制用户名,配置位于:

\Drupal::config('user.flood');

该配置对象各键含义如下:

ip_limit:

id错误阀值,默认50

ip_window:

ip错误时间控制,默认3600秒(1小时),即默认在过去的一小时内,如果同一ip错误达到50次,即暂时锁定登录,用户需要稍微等待再登录,等待时间不是固定的,以数据库中查询到的错误次数小于阀值为准,详见flood服务原理

user_limit:

特定用户登录错误阀值,默认为5

user_window:

特定用户错误时间控制,默认为21600秒(6小时),含义同上,即过去6小时内累计错误达5次即暂停登录

uid_only:

布尔值,用于指定频率限制的用户标识符是否仅基于用户id,默认为false,此时用户标识符为uidip,错误次数阀值仅应用到该ip,如果为true,则不会附加ip,即表示不论用户在哪里登录,只要错误达到阀值即暂停登录,这是相当严格的,也是最安全的,但通常并不推荐这样做,因为恶意用户可以故意多次登录失败,这样导致合法用户的登录也被暂停

三、然后调用用户凭证鉴别服务(服务iduser.auth)进行密码比对

四、记录日志

五、设置登录后转向地址,默认转向路由“entity.user.canonical”,如需指定登录后的转向地址,在登录表单中添加destination参数即可,其值为转向地址

六、成功时,取得用户实体对象后,调用以下方法完成登录:

  user_login_finalize(UserInterface $account)

在该方法中将进行一些必须的事务:

设定当前账户、更新账户最后登录时间、在会话数据中写入uid、重新签发会话id(这是为了防止会话固定的风险,不只是drupal,任何系统均需要这样做)、派发用户登录钩子“user_login”(参数为用户实体对象)

 

自定义登录:

通过前文登录过程的介绍,开发者应已明白需要做那些事情,在默认情况下系统是通过用户名和密码进行登录的,然而你可能想要通过一些其他办法登录,比如仅凭手机号码和短信验证码登录,又比如以邮件、手机、用户名三者中任意一个配合密码登录,需要做到这些只需要定义一个表单即可,参照前文登录过程进行必要的检查,确定可以登录后执行以下函数即可:

user_login_finalize(UserInterface $account)

 

用户登录块:

块插件iduser_login_block

插件类:Drupal\user\Plugin\Block\UserLoginBlock

用于提供一个用户登录块,在登录状态下不显示,使用了前文提到的默认用户登录表单:

Drupal\user\Form\UserLoginForm

因此登录逻辑和默认的用户登录完全一样

 

退出登录(登出)过程:

登出的路由定义如下:

user.logout:
  path: '/user/logout'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::logout'
  requirements:
    _user_is_logged_in: 'TRUE'

该路由仅在登录状态下才能访问,访问即会执行登出动作,不会进行确认提醒。该控制器也用于提供重置密码、查看账户等功能

登出过程比较简单,仅需调用以下方法和设置跳转即可(默认跳转到首页):

user_logout();

开发者应调用该方法进行登出,比如在维护模式中即调用该方法登出用户,在该方法中按顺序做以下事情:

一、记录日志

二、派发钩子“user_logout”,钩子参数为账户对象(会话账户对象或用户实体)

三、销毁会话数据,这意味着会话数据中的所有数据都会消失,而不仅仅是UID

四、设置当前账户为匿名用户账户

 

默认重置密码过程:

系统默认的重置密码功能通过多次路由跳转实现,入口页面路由定义如下:

user.pass:
  path: '/user/password'
  defaults:
    _form: '\Drupal\user\Form\UserPasswordForm'
    _title: 'Reset your password'
  requirements:
    _access: 'TRUE'
  options:
    _maintenance_access: TRUE

该路由不管登录状态如何,均可无条件访问(登录用户修改密码或邮件需要原密码,在忘记时也需要重置),功能由以下表单提供:

\Drupal\user\Form\UserPasswordForm

这里同样针对ip及用户名做了频率限制,限制配置和登录限制一样,不同的是针对用户名的频率限制直接采用用户id,意味着不管用户在哪里,默认情况下过去6小时内达到5次即会暂时不允许重置。

用户可输入邮件地址或用户名来重置密码(前后空白字符会被自动先去掉),系统优先将用户输入当做邮件进行账户查询(如果是已登录状态则直接使用用户邮件当做用户输入),如没有再当做用户名去查询,如果没有找到用户数据将提示错误,如果找到了会判断账户是否处于阻塞状态,如果是则会给出错误并中断处理,否则调用以下函数发送重置密码引导邮件:

_user_mail_notify('password_reset', $account, $langcode);

该函数用于统一处理用户模块的各类型邮件发送(原理见本系列邮件系统主题),最终通过以下函数计算一个一次性登录链接发送到用户邮箱:

user_pass_reset_url($account, $options = [])

链接类似如下:

http://www.dp.com/user/reset/6/1576208771/Eb8v6IyU41lq1rbVxq-MFvdPA32_7FM0Oa4meYpb50g

用户通过该链接可免密码登录系统,但该链接有有效期(默认一天),且有效期内仅能登录一次,一旦实际登录即失效,该链接包含用户id、重置时的时间戳、哈希验证码,对应以下路由:

user.reset:
  path: '/user/reset/{uid}/{timestamp}/{hash}'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::resetPass'
    _title: 'Reset password'
  requirements:
    _access: 'TRUE'
  options:
    _maintenance_access: TRUE
    no_cache: TRUE

该路由尚不验证一次性链接的有效性,主要功能是检查打开链接时站点的登录状态,如果已登录则给出错误提示(如果登录的是重置账号则退出登录再重新访问本路由),如果未登录则跳转到以下路由:

user.reset.form:
  path: '/user/reset/{uid}'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::getResetPassForm'
    _title: 'Reset password'
  requirements:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE
    no_cache: TRUE

此路由仅在未登录状态下才能访问,将展示以下表单:

\Drupal\user\Form\UserPasswordResetForm

该表单向用户展示一些关于密码重置的说明,当点击登录按钮时,将跳转到以下路由:

user.reset.login:
  path: '/user/reset/{uid}/{timestamp}/{hash}/login'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::resetPassLogin'
    _title: 'Reset password'
  requirements:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE
    no_cache: TRUE

在该路由中系统才真正检查一次性登录链接的有效性,注意:哈希码的生成见以下函数:

user_pass_rehash(UserInterface $account, $timestamp)

由于时间戳信息被参入了哈希码,因此时间戳不能被人为改变,验证时,时间戳会和用户最后登录时间做比较,这样保证了在有效期内一次性登录链接只能使用一次。

如果一次性登录链接验证通过,那么会登入用户,并重定向到用户实体编辑路由entity.user.edit_form进行密码重设,该路由采用实体表单user.default,表单如下:

Drupal\user\ProfileForm

注意:如果以实体表单构建器方式直接构建该表单还是会要求输入原密码,但通过该路由并传递了密码重置token 就不再要求输入原密码了,即不要求输入原密码的方法如下:

        $user=\Drupal::entityTypeManager()->getStorage("user")->load(75);
        user_login_finalize($user);
        $token = \Drupal\Component\Utility\Crypt::randomBytesBase64(55);
        $_SESSION['pass_reset_' . $user->id()] = $token;
        return $this->redirect(
            'entity.user.edit_form',
            ['user' => $user->id()],
            [
                'query' => ['pass-reset-token' => $token],
                'absolute' => TRUE,
            ]
        );

在控制器中执行即可(要求控制器继承控制器基类),地址类似如下:

http://www.dp.com/user/6/edit?pass-reset-token=mBKmQIeVZy4hWV0O-witeBq7sZcB-YjT5DKGjJRrNi0qnBBP1pQcO4-2lQI97qEEOVV_Osad9Q

 

自定义密码重置:

默认密码重置是通过邮件验证并重置的,这要求有用户邮件,且过程稍微繁杂,你可能需要更快捷的方式,在中国典型的是通过密码保护问题或手机验证码去重置,这如何实现呢?其实非常简单:

实现一个在登出状态下能访问的表单路由,实施频率控制,在表单中进行用户真实性验证(比对密保问题或验证手机验证码),验证成功后直接设置密码:

$userEntity->setPassword($newPassword)->save();

并做日志记录,和用户重定向即可

 

AJAX登录、登出、重置密码、登录状态查询

如果需要以AJAX方式实现登录、登出、重置密码、登录状态查询,那么用户只需要负责实现前端即可,后端已提供了各功能的路由,这些路由统一使用以下控制器:

\Drupal\user\Controller\UserAuthenticationController

各功能路由为:

登录:

路由名:user.login.http

控制器方法:\Drupal\user\Controller\UserAuthenticationController::login

POST方式接收用户名和密码,表单名分别是namepass,如果认证成功则签发新的会话id,系统转变为登录状态,并以JSON方式返回用户相关信息,包含在键名current_user下,如果有此键名说明登录成功,各信息有:uidrolesname,如果登录失败则返回异常;AJAX同样运用了频率控制

退出登录:

路由名:user.logout.http

方法:\Drupal\user\Controller\UserAuthenticationController::logout

POST方式接收一个请求即可,不需要任何数据提交,系统退出登录,返回一个204状态响应,意为执行成功,但没有任何响应内容。该路由仅在登录状态下能访问

重置密码:

路由名:user.pass.http

方法:\Drupal\user\Controller\UserAuthenticationController::resetPassword

POST方式接收用户名或电子邮件,表单名分别是namemail,系统将发送重置引导邮件给用户,成功时返回200状态响应,但没有显示内容,出错时(用户不存在、处于阻塞状态、邮件发送不成功等)返回异常,该路由同样有频率控制

状态查询:

路由名:user.login_status.http

方法:\Drupal\user\Controller\UserAuthenticationController::loginStatus

GET方式访问,请求格式为json,返回一个text/plain类型的响应,1为已登录,0为未登录

 

账户相关系统邮件:

一些和账户相关的系统邮件,邮件类型标识符及含义如下:

register_admin_created:欢迎消息,通过管理员注册时发送
register_no_approval_required:欢迎消息,用户自行注册时发送
register_pending_approval:欢迎消息,用户自行注册但需要管理员审核时发送
password_reset:重置密码时发送
status_activated:账户被激活时发送
status_blocked:账户被阻塞时发送
cancel_confirm:账户注销需要通过邮件确认时发送
status_canceled:账户注销后发送

这些默认邮件统一由以下函数处理:

function _user_mail_notify($op, $account, $langcode = NULL)

参数及其含义如下:

$op:前文所述操作名

$account:账户对象(用户实体),至少包含用户id、用户名、邮件地址

$langcode:语言代码,如不传递将采用用户账户中设定的首选语言

发送模板见配置对象“user.mail”,邮件内容处理见邮件钩子函数user_mail

返回布尔值或NULL,为TRUE时代表投递成功,NULL代表没有发送,FALSE代表发送失败;模块可取消发送,具体发送细节原理请见本系列邮件系统主题

 

修改被显示的用户名:

这很常用,比如追加先生、女士等称谓,又或者以昵称方式显示,在需要显示用户名的地方应该通过账户对象的getDisplayName方法取得,系统在该方法中派发了钩子:

\Drupal::moduleHandler()->alter('user_format_name', $name, $account);

其中$name为登录用户名,$account可能是会话账户对象:

\Drupal\Core\Session\UserSession

也可能是用户实体

如需修改显示的用户名实现该修改钩子即可

 

用户数据服务:

服务iduser.data

类:Drupal\user\UserData

获取方式:\Drupal::service('user.data')

这是一个很简单的模块,用于帮助各模块辅助性的储存某用户的数据,任何模块在储存和具体某用户相关的数据时,推荐使用该服务,使用的数据库表为“users_data”,该表在用户模块安装时建立,各字段含义如下:

uid:用户id

module:数据所属模块的名字,通常是该模块建立的数据

name:数据键名

value:所存的数据,二进制格式储存

serialized:所存数据是否经过序列化处理,10,标量数据直接储存,反之以序列化方式储存

该服务提供了以下方法:

public function set($module, $uid, $name, $value)

储存数据,参数依次为模块名、用户id、数据键名、所存的数据本身,无返回值

public function delete($module = NULL, $uid = NULL, $name = NULL)

删除数据,参数含义同上,每个参数都可以数组方式指定多个,她们将作为删除条件,每一个参数都是可选的,这带来极大灵活性

public function get($module, $uid = NULL, $name = NULL)

获取数据,仅模块参数是必须的,返回值因传递的参数不同而不同,如全部传递将返回储存的数据,在没有数据时返回NULL,否则返回数组值:如果uid被传递那么以name值做键名,如果name被传递,那么以uid值做键名,如果都没有传递,那么第一级键名为uid值,第二级为name值,值均为被储存的数据(如果有序列化则已经解序列化)

注意:当一个模块被卸载时,会派发模块卸载钩子,用户模块实现了该钩子,在其中会删除该模块在此服务中的数据,见:user_modules_uninstalled($modules)

 

补充:

1、如果用户状态被管理员在后台修改,那么会依据配置可选的发送用户邮件,当被改为阻塞时,会立即强制用户退出登录,即用户下一个请求时将处于非登录状态

2、用户最后访问时间将在“onKernelTerminate”事件中设置,保存的是请求时间

添加新评论

受限制的 HTML

  • 允许的HTML标签:<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • 自动断行和分段。
  • 网页和电子邮件地址自动转换为链接。
请输入以上问题的答案,换一个问题请刷新,不区分大小写