14. 认证Authentication系统、认证提供器
在drupal8中如何判定请求来源于已认证用户?不带认证信息的请求视作匿名用户,带认证信息的所有请求都需要认证,不能通过认证的请求显示质询界面,会话ID就是一种认证信息。那么是何时又是如何认证的呢?这就是本篇的主题。
认证系统是在何时开始运作的呢?它是在核心派发kernel.request事件时触发的,在认证订阅器AuthenticationSubscriber中完成。这个时机是比较早的,在得到控制器之前就已完成。
通过认证的请求会建立账户对象(不带认证信息的请求也会建立匿名账户对象)后续程序通过这个账户对象就可以方便的知道账户ID、用户名、昵称、邮件、角色、权限情况、语言偏好、时区设置、最后访问时间等等,此对象代表登录状态。
关于认证系统的主要代码存放在:\core\lib\Drupal\Core\Authentication文件夹下,我们来看一看:
首先需要明白的是真正的认证工作是被叫做“认证提供器 Authentication Provider”的对象来完成的,这个文件夹下的内容围绕它展开,但此处并无用于工作的认证提供器,它由核心模块或用户定义。
为规范使用,定义了几个接口:
AuthenticationProviderInterface:所有的认证提供器必须要实现的接口,里面仅定义了两个方法:
public function applies(Request $request); 检查请求是否携带用于认证的信息。
public function authenticate(Request $request); 用请求携带的认证信息去检查能否通过认证。
AuthenticationProviderFilterInterface:认证提供器过滤接口,检查认证方法能否被用于某个路由上面:
public function appliesToRoutedRequest(Request $request, $authenticated);
AuthenticationProviderChallengeInterface:认证提供器质询接口,当认证不通过时产生一个质询
public function challengeException(Request $request, \Exception $previous);
系统提供了一个认证管理器AuthenticationManager,同时实现以上三个接口。
AuthenticationCollectorInterface:认证提供器聚集器接口,用于统一管理认证提供器,并区分它们的优先级。
AuthenticationCollector是其默认实现。
以上是文件用途,我们看看具体运作:
在drupal8中认证账户对象需要由认证提供器来建立,否则默认是匿名用户对象,要想得到已登录的用户账户对象,就需要先收集认证提供器,怎么收集呢?看认证提供器聚集器的服务定义:
authentication_collector:
class: Drupal\Core\Authentication\AuthenticationCollector
tags:
- { name: service_collector, tag: authentication_provider, call: addProvider }
它的标签表示它实例化后将所有带有标签名authentication_provider的服务传递给addProvider方法调用一次,这种标签用法见编译器:\core\lib\Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass.php
drupal8还提供了一个编译器:\core\lib\Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass.php它用于给容器添加一个参数authentication_providers,此参数数组包含了系统当前存在的服务提供器。
如果标签为authentication_provider的服务带有标签属性,会将标签属性一并传入
系统默认只定义了一个认证提供器,它的服务id是user.authentication.cookie,在user模块中实现,来看看它的定义:
user.authentication.cookie:
class: Drupal\user\Authentication\Provider\Cookie
arguments: ['@session_configuration', '@database']
tags:
- { name: authentication_provider, provider_id: 'cookie', priority: 0, global: TRUE }
以上就是服务提供器的定义和收集工作。
AuthenticationCollector包含着系统提供的认证提供器,它作为参数传递给AuthenticationManager来进行认证管理,并在AuthenticationSubscriber中得以运行,服务提供器是有优先级的,从高到低依次检查对某一请求是否可用,一旦可用即停止检查并返回作为默认认证提供器,低优先级的提供器没有机会运行,系统提供的默认提供器优先级为0,提供器在进行认证操作后,如果通过则返回账户对象,否则返回空,下面我们看一看系统默认提供的认证提供器:
它位于:\core\modules\user\src\Authentication\Provider\Cookie.php
此认证提供器检测请求是否包含正确的会话ID,如果有,则从会话中取回用户ID并产生账户信息,核心代码如辅助内容区:
从这个方法中你可以清楚的看出用户账户对象是如何形成的,在全局使用中会用一个账户代理对象AccountProxy封装它。如果是一个已经登录的用户,此时(核心派发kernel.request事件时)系统中将存在一个用户账户对象。以上就是认证及账户对象形成过程。
/**
* Returns the UserSession object for the given session.
*
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The session.
*
* @return \Drupal\Core\Session\AccountInterface|null
* The UserSession object for the current user, or NULL if this is an
* anonymous session.
*/
protected function getUserFromSession(SessionInterface $session) {
if ($uid = $session->get('uid')) {
// @todo Load the User entity in SessionHandler so we don't need queries.
// @see https://www.drupal.org/node/2345611
$values = $this->connection
->query('SELECT * FROM {users_field_data} u WHERE u.uid = :uid AND u.default_langcode = 1', [':uid' => $uid])
->fetchAssoc();
// Check if the user data was found and the user is active.
if (!empty($values) && $values['status'] == 1) {
// Add the user's roles.
$rids = $this->connection
->query('SELECT roles_target_id FROM {user__roles} WHERE entity_id = :uid', [':uid' => $values['uid']])
->fetchCol();
$values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids);
return new UserSession($values);
}
}
// This is an anonymous session.
return NULL;
}