“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
密码储存需要满足两方面要求:安全性和可升级
安全性:
系统安全来自外部和内部,有资料显示攻击事件主要来自内部,如已离职人员,大型项目内部人员更替势必发生,因此密码绝不可用明文方式储存,通常采用散列算法,也就是储存哈希值,密码哈希不是密码加密,加密是可以逆转的,换句话说是可以解密为明文的,但哈希具有单向性,不可逆转,即便得到数据库也无法得知用户密码;密码太短容易被猜测,这很容易理解,但太长也不行,太长意味着需要更多资源进行哈希计算,容易引起性能攻击(DOS攻击),因此需要限定明文密码字符数;在实现上也需避免各种攻击,比如时序攻击(通过耗时多少去猜测密码长度)、暴力破解(常用密码猜测)等等。
可升级:
安全性遵循木桶效应,只需一个地方短缺,水就装不满,比如以前常用MD5算法来产生哈希值,现在高安全性系统基本不用了,因此一旦发现弱点就需要升级,而除了用户外没有人知道密码明文,此时在密码的储存上就需要有可升级机制,通常做法是给密码算法指定版本号,并将版本号和密码哈希一起储存,在用户重新登录时更新哈希,这种应对升级的做法称为制定密码模式(密码scheme),模式可以很简单,也可以很复杂,在模式中可以指出密码的很多信息。
接下来我们看看drupal如何处理密码的安全性和可升级
Drupal默认密码scheme:
为方便叙述,本文将储存在数据库中经过哈希处理的密码称为密码储存码,简称储存码,Drupal使用的密码明文最长不超过512字节,可以是任意字符,储存码固定为55字节,仅包含以下字符:
$ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
储存码由以下四部分组成(前三部分可看做是密码的scheme,或称为密码设置字段):
第一部分:密码版本标识:
占3字节,“$S$”表示以sha512算法做哈希运算,“$H$”或“$P$”表示采用md5算法,最新生成的密码默认采用“$S$”,即sha512算法
第二部分:哈希强度指示:
占1字节,记录哈希迭代强度,是前文$ITOA64变量字符串中的一个字符,她决定了对密码应用哈希算法的次数:以该字符在$ITOA64变量字符串中的位置(键名)当做2的指数计算的值就是运用哈希算法的重复(迭代)次数;默认为E,在$ITOA64变量字符串中对应键名为16,即迭代次数为2的16次方,这将对密码加盐反复应用哈希算法65536次,计算量是很大的,因此系统限定了该项为2的7次方到30次方之间(含两端),实测默认强度生成十组密码用时1.7秒(i5处理器,8GB内存,64位系统)
第三部分:哈希盐:
占8个字节,哈希盐是自动随机生成的,每个用户密码均不相同
第四部分:密码哈希:
占43字节,是加盐迭代运算出的密码哈希值,可能是截取的结果,是否截取视算法而定,比如sha512将截取,而md5不会截取
Drupal的密码模式有以下特点:
1、密码相同储存码不同,然而储存码相同则密码一定相同,密码和储存码是一对多关系
2、无法依据drupal密码算法预先生成密码字典,因为哈希盐并不固定,每个密码的盐都是随机生成的
3、验证用户明文密码时需要密码的储存码,因为哈希盐等储存在储存码中
密码服务:
在drupal中密码的哈希运算、比较、升级判断是在密码服务中进行的,该服务实现了以上默认密码scheme,定义如下:
服务id:password
类:Drupal\Core\Password\PhpassHashedPassword
接口:\Drupal\Core\Password\PasswordInterface
各方法介绍如下:
public function hash($password)
参数为明文密码,返回经过运算的密码储存码,数据库中密码字段即存储该方法的返回值
public function check($password, $hash)
检查密码是否正确,返回布尔值,参数$password为明文密码,参数$hash为经过运算转化的储存码,也就是数据库中密码字段保存的值
public function needsRehash($hash)
依据密码scheme判断是否需要升级储存码,参数为密码的储存码,其前12字符为设置字段,出现以下三种情况即被认为需要升级:
不以“$S$”开始,说明需要算法升级
储存码长度不为55
迭代次数(哈希强度)已改变
protected function generateSalt()
生成哈希盐,准确说是生成一个储存码前缀,也叫密码设置字段,返回一个12字节的字符串
protected function base64Encode($input, $count)
该方法从一个密码学强度的随机字符串中截取指定字节转化为可显示的字符返回,其中参数$input为一个密码学强度随机生成的字符串,通常采用php函数random_bytes生成,这里虽然将其称为字符串,但她并不对应任何字符集,因此直接打印会乱码,参数$count意为从$input的第一字节开始,一共截取多少字节进行转化,因此取值不能大于$input的长度,1字节等于8比特,返回的字符串仅包含以下静态变量中的字符:
public static $ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
该静态变量在前文也提到过,是经过精心选择的:
和base64字母表类似(不同的是把加号替换成了点号,字符顺序也不同),一共64个字符,数组方式访问时最大键名为63,而63的十六进制表示为“0x3f”,二进制表示为6个1,即“111111”,这和任何数的“与”操作等同于截取该数的低六位,这和一个完全随机的数进行与操作就能得到一个完全随机的大于等于0,小于等于63的数,这样就能在不丢失随机性的前提下,通过该静态变量转化得到一个随机的可显示的字符串。
该方法每轮循环处理3字节,也就是24比特的随机数,这是因为24是8和6的最小公倍数。
返回字符数的长度为:对“$count*8/6”进行向上取整的结果,如:
$count为1,结果为2
$count为2,结果为3
$count为3,结果为4
$count为5,结果为7
$count为6,结果为8
protected function crypt($algo, $password, $setting)
运算返回密码的储存码,参数含义如下:
$algo:哈希算法,默认为sha512
$password:明文密码
$setting:密码设置字段,或完整的储存码
在出现错误时返回false,此时应视为密码不正确
public function getCountLog2($setting)
返回哈希应用次数以2为底的对数,即返回值作为2的指数进行计算,结果为哈希算法迭代的次数,参数为一个字符串,密码设置字段
在系统架构上为了提供更高层次的封装,在登录时不会直接调用密码服务,而使用用户凭证鉴别服务,由该服务来调用密码服务,其定义如下:
服务id:user.auth
类:Drupal\user\UserAuth
接口:\Drupal\user\UserAuthInterface
该服务较简单,仅一个方法:
public function authenticate($username, $password)
用于验证用户的密码是否正确,参数为用户名(用户实体的name字段)以及明文密码,当验证成功时返回用户id,否则返回false,当验证成功时会检查并升级密码scheme,核心工作由密码检查器完成
密码字段:
字段类型id:password
类:Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem
该字段类型不在UI中显示,特别之处是有三个属性,但数据库仅储存其中一个,用户实体默认使用该字段,她提供了以下两种能力:
保存明文密码:
该字段的“pre_hashed”属性是一个布尔值,指示被注入的密码是否已经进行过哈希转化,如果为true,那么不会再进行转化,如下代码将会保存密码明文到数据库:
$userID=1;
$userEntity=\Drupal::entityTypeManager()->getStorage("user")->load($userID);
$userEntity->get('pass')->pre_hashed=true;
$userEntity->setPassword('yunke')->save();
数据库密码字段的值将是“yunke”,但设计这种能力并不是用来随意保存明文的,而是使得注入已经被哈希处理的储存码时,不被再次哈希,如果随意保存非储存码的明文,虽然能保存成功,但因其不符合密码模式,会导致无法登陆,是无意义的。
验证密码:
在用户改变密码、邮件、用户名时应进行密码验证,因此用户实体提供了以下方法:
public function setExistingPassword($password)
该方法要求提供明文密码,提供后可用以下方法验证:
public function checkExistingPassword(UserInterface $account_unchanged)
为了向上支持这样的用法,密码字段提供了“existing”属性用以暂时保存明文密码
注意:
密码字段没有默认控件和格式化器,控件由表单直接采用password_confirm元素类型渲染(见下文),密码不需要显示因此不需要格式化器,由此也可看出字段类型不是必须指定控件和格式化器
密码元素类型:
基本类型:
类型id:password
元素类:Drupal\Core\Render\Element\Password
该类型很简单,通过模板输出一个密码表单
带确认的密码表单:
类型id:password_confirm
元素类:Drupal\Core\Render\Element\PasswordConfirm
该类型将以基本密码类型类型元素来构建两个密码表单,以实现密码再次输入的确认功能,如果输入不一致表单将无法通过验证
忘记密码的处理:
通常忘记密码可以使用密码重置功能通过邮件找回,但有时可能无法使用邮件,此时可以使用本节的方法找回密码。
在能登录服务器主机时
可以随意找一个页面能访问的控制器,更改其php文件,通过以下代码重置密码:
$userID=1;
$userEntity=\Drupal::entityTypeManager()->getStorage("user")->load($userID);
$newPassword='yunke';
$userEntity->setPassword($newPassword)->save();
这种方式需要注意控制器是否真的被执行到,有些页面有缓存,控制器通常并不执行,可查找路由选项中有“no_cache: TRUE”的控制器,比如路由“user.reset.form”的控制器:
\Drupal\user\Controller\UserController::getResetPassForm
在仅能访问数据库时
可以通过另外安装的Drupal站点用密码服务重新生成一个密码并替换数据库,生成代码如下:
$newPassword='yunke';
$storedHash=\Drupal::service('password')->hash($newPassword);
echo $storedHash;
替换数据库表“users_field_data”的“pass”字段
或者直接使用以下储存码去替换:
$S$EWXgYLwRwElnArr6tDUGs0HsedDQ6okTGbjxHt5fhfFDb6Maf0dW
该储存码是通过以上代码生成的,替换后明文密码为“yunke”。
该方式注意:
在替换数据库值后,你可能还无法登陆,这是因为实体对象的储存有缓存机制,此时需确保缓存被清除,可找到数据库缓存表“cache_entity”,在字段cid中查找值“values:user:1”(1为用户id),如果找到删除即可,如未找到说明尚未缓存,清除缓存后即可登录了
使用模块:
如果你站点中安装了本系列配套模块“yunke_help”,那么在模块首页的快捷操作中,可以免密码验证直接更改本账户密码,但前提是你有权使用该模块,该功能通常给开发者使用
补充:
1、在系统中任何地方明文密码都会被运用php函数trim去除首位空白字符后再处理
2、得到用户的密码储存码:$userEntity->getPassword();
3、如果用户的密码被改变,那么会删除之前的所有会话数据,如果是当前用户会同时更新会话id,在那改变
4、如果数据库密码字段为空,那么用户无法登录
反馈互动