从一开始,PHP 就是一种为构建网站而生的编程语言。这一理念植根于 PHP 核心的深度比任何其他编程语言都要深,这或许就是 PHP 在 Web 应用程序构建领域变得且保持如此流行的一个原因。但是,当 PHP 于上世纪 90 年代中期首次设计出来时,术语 Web 应用程序 甚至还不存在。因此,密码保护不是 PHP 创建者投入资源设计的功能之一。毕竟,在只是使用 PHP 在网页上植入站点访问计数器或一个修改日期戳时,您不必再担心密码问题。
但 20 年过去了,现在,人们几乎无法想象创建一个不涉及受密码保护的用户帐户的 Web 应用程序。PHP 程序员最重要的工作就是使用最新、最安全的方法来保护帐户密码。为此,PHP 5.5 添加了一个由 Anthony Ferrara ( @ircmaxell ) 创建的新的密码哈希库。该库提供了一些函数,您可使用它们通过目前的最佳实践方法来处理单向密码加密。其他功能旨在满足未来的安全需求,以便随着计算机和攻击者水平更加进步,您始终可抢先坏人一步。本文将深入介绍该库的函数,以及如何最有效地使用它们。
PHP 作为一个活跃的开源项目在不断的改进,目前为许多 Web 网站提供支持。PHP 早期是一种模板语言,从那时到现在,PHP 已经历了显著的变化。如果您多年没有使用过或评估过 PHP 技术,那么您很可能几乎认不出当前的一些 PHP 项目。本系列文章将向您展示最新的 PHP 功能,以及如何使用当今的 PHP 来构建现代化的、安全的网站。
“ 像对待牙刷一样对待您的密码。不要让其他任何人使用它,并且每隔 6 个月换一个新密码。 ”
Clifford Stoll
以下建议或许不言自明(至少我希望是这样):绝不将用户的密码存储为明文。始终使用哈希算法等单向加密算法存储密码的加密版本,以使任何能够访问您的帐户数据库的人都无法发现您用户的密码。此警告不仅适用于保护用户远离通过获取数据库访问权来损害您的网站的人。您还需要防御您组织内拥有恶意企图的人。
哈希算法 获取一个任何长度的字符串并采用一致的方式创建一个 哈希值 :一种固定长度的字符串表示。每次传入同一个原始字符串,您都会收到相同的哈希值。这是一个单向过程,您无法从中获得原始字符串。
此需求变得越来越紧迫,因为不幸的是,许多用户对多个网站使用相同的密码。如果有人能够访问您的某个用户的电子邮件地址和原始密码,他很可能能够访问该用户在其他网站上的帐户。
所有哈希值不是以同一种方式创建。那么您就可以使用各种算法来创建哈希值。过去常用的两个算法是 MD5 和 SHA-1 。如今强大的计算机很容易破解这两种算法。举例而言,已有 软件 能够在单个 GPU 上以每秒 36.5 亿次计算的速率破解 MD5 的哈希值,以每秒 13.6 亿次计算的速度破解 SHA-1 的哈希值。依据这样的速率,根据密码的复杂性和长度,可以在不到半小时内破解一个哈希值。
所以,使用在计算上更复杂的哈希算法很重要。您不仅想要更长的哈希值(这会减少哈希碰撞的机会,也就是说,减少两个短语生成相同的哈希值的机会),您还希望生成哈希值所花的时间尽可能长。为什么?对于您自己的 Web 应用程序,用户每次登录时仅需等待生成一个密码哈希值一次。如果等待时间持续 1 秒(或者甚至两秒),用户不会关心,甚至不会注意到。但通过将破解尝试从每秒 36 亿此计算的减慢到每秒 1 次,任何尝试的难度都会呈指数级增长。
您还需要防御 彩虹表 。彩虹表(比如您可以在 md5cracker.org 上访问的 MD5)是哈希值的逆向查找表。该表的创建者预先计算所有常见单词、短语、修改的单词,甚至随机字符串的 MD5 哈希值。能够访问某个哈希值的人可在查找表中输入它,发现用于生成它的密码,从而有效地反转这个单向过程。破解 MD5 哈希值的相对较低的处理成本使得创建彩虹表成为了可能。
为一个计算上复杂的算法生成彩虹表需要花长得多的时间。但仍然可能做到,而且创建者只需付出一次性的努力。合适的应对措施是向您的哈希值添加 SALT 。在此上下文中, SALT 指的是任何在创建哈希值之前首先添加到您的密码中的短语。通过使用 SALT,您最终会(在实际中)战胜彩虹表。其他人需要生成一个特定于您的应用程序的彩虹表,然后破解多个密码,才会找到 SALT是什么 — 这是一种复杂的、代价高昂的场景。
回页首
现在看一下清单 1,这是几年前 PHP 中有效的密码实践的一个示例。
清单 1. PHP 中通常被视为有效的密码安全做法
<?php // Create a password class to handle management of this: class Password { const SALT = 'MyVoiceIsMyPassport'; public static function hash($password) { return hash('sha512', self::SALT . $password); } public static function verify($password, $hash) { return ($hash == self::hash($password)); } } // Hash the password: $hash = Password::hash('correct horse battery staple'); // Check against an entered password (This example will fail to verify) if (Password::verify('Tr0ub4dor&3', $hash)) { echo 'Correct Password!/n'; } else { echo "Incorrect login attempt!/n"; }
阅读: php.net 上的 hash() 文档
像清单 1 这样的示例在 Web 中被视为所谓的最佳实践。长时间以来,此方法 曾是 最佳实践 — 显然比使用 MD5 更好,而且比将密码存储为明文要好得多。清单 1 使用了复杂得多的 SHA-512 算法,而且它强制所有密码都添加上了 SALT,以战胜预先制定的彩虹表。但此方法仍然存在一些问题。
清单 1使用了一个 SALT,但每个密码都使用完全相同的 SALT。所以,一旦有人破解了一个密码(或者更糟地,通过访问代码库而发现了 SALT),那么他就可以通过向每个采用表条目添加该 SALT来创建自定义的彩虹表。战胜彩虹表的解决方案是,在创建密码时为每个密码使用一个随机 SALT,将 SALT和密码一起存储,以便可获取密码。
清单 1也使用了 SHA-512(PHP 随带的一种复杂得多的算法),而没有使用 MD5 或 SHA-1。但是,甚至 SHA-512 哈希值也可以每秒 4600 万次计算的速率破解。尽管比 MD5 或 SHA1 破解速率更慢一些,但此速率对足够的安全性而言仍是不够的。此问题的解决方案是,使用在计算上更复杂的算法,而且多次使用这些算法。例如,为每个密码连续运行 SHA-512 100 次,这会显著减缓任何攻击尝试。
好消息是,您不需要尝试使用自己的代码实现此解决方案。PHP 5.5 中新的密码哈希计算库解决了这一问题。
回页首
password_hash()
这个密码哈希扩展为您创建在计算上复杂的安全的密码哈希值,包括在幕后生成和处理随机的 SALT。在对您想要计算哈希值的密码调用 password_hash()
的最简单用例中,该扩展会为您处理所有事情。您还需要提供第二个参数:您想要扩展使用的哈希算法。您有两种选择,但在目前,指定 PASSWORD_DEFAULT
常量是最佳选择(我稍后会解释其中的原因):
<?php $hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);
也可提供第三个参数,这是一个更改哈希值生成方式的选项数组。您可以在这里指定 SALT,但最好是不指定 SALT,并允许为您生成随机 SALT。更重要的是,在这个数组中,您可以指定一个 cost
值。此值(默认值为 10
)可以确定该算法应多复杂,进而确定生成哈希值将花费多长时间。(将此值视为更改算法本身重新运行的次数,以减缓计算。)如果想要一个更安全的密码,而且您的计算机能够处理它,您可以像这样执行调用:
<?php $hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT, ['cost' => 14]);
使用我自己的 MacBook Pro 作为测试环境,生成一个 cost 为 10
(默认值)的 password_hash
大约会花 0.085 秒的时间。将该 cost 调高到 14
,会将该时间更改为每次计算 1.394 秒。
因为前面两个示例中的密码是使用一个随机 SALT 过程生成的,所以我无法直接知道相关的 SALT。因此,如果我尝试再次运行 password_hash()
并比较字符串,以此作为验证密码的方式,结果将会不匹配。每次您调用该函数,都会生成一个新 SALT,返回的哈希值也不同。所以该扩展提供了第二个函数 password_verify()
,它为您处理验证过程。您调用 password_verify()
,传入用户所提供的密码和存储的哈希值,如果密码是正确的,该函数返回一个布尔值 TRUE
,否则返回 FALSE
:
<?php if (password_verify($password, $hash)) { // Correct Password }
现在我可以重构清单 1中的类,使用内置的密码哈希扩展,如清单 2 所示。
清单 2. 重构清单 1 的 Password
类
<?php class Password { public static function hash($password) { return password_hash($password, PASSWORD_DEFAULT, ['cost' => 14]); } public static function verify($password, $hash) { return password_verify($password, $hash); } }
阅读: php.net 上的密码哈希文档
回页首
通过使用新的密码哈希扩展,您可以将您的代码库提升到如今的安全标准水平。但仅在几年前,专家曾说过,SHA-1 是一种最佳实践解决方案。那么,如果它不是最佳实践, 当 密码加密需要更强时,会发生什么?幸运的是,新扩展有一个内置的功能考虑了这一可能性。
可以使用 password_needs_rehash()
函数(在幕后)检测存储的密码是否与您指定的当前安全需求相匹配。如果不匹配,原因可能是,您增加了 cost
参数,或者一个新的 PHP 版本在幕后更改为一种不同的哈希算法。正因如此, PASSWORD_DEFAULT
应当是您首选的算法;此选项始终会使您的软件使用当前的最佳实践版本。
此功能的使用更加复杂,但不是过于复杂。核对用户的密码时(比如用户尝试登录时),您需要执行一个额外的任务:调用 password_needs_rehash()
,它接受与 password_hash()
类似的参数。 password_needs_rehash()
函数针对新请求的安全设置来检查所提供的密码哈希值。如果密码哈希值与这些设置不匹配,那么该函数会向您报告这一事实。
一些程序员在这里难以理解,因为 password_needs_rehash()
函数所做的一切都是为了让您知道密码是否需要重新计算哈希值。然后是否生成密码的新哈希值并保存它,这完全取决于您,因为密码扩展不知道您需要如何存储密码。
在清单 3 中,我提供了一个完整的模拟的 User
类,在这个类中,通过使用我讨论的工具,既能安全地处理用户的密码,又能支持未来不断变化的安全需求。
清单 3. 这个模拟的 User
类显示了密码扩展的完整用途
<?php class User { // Store password options so that rehash & hash can share them: const HASH = PASSWORD_DEFAULT; const COST = 14; // Internal data storage about the user: public $data; // Mock constructor: public function __construct() { // Read data from the database, storing it into $data such as: // $data->passwordHash and $data->username $this->data = new stdClass(); $this->data->passwordHash = 'dbd014125a4bad51db85f27279f1040a'; } // Mock save functionality public function save() { // Store the data from $data back into the database } // Allow for changing a new password: public function setPassword($password) { $this->data->passwordHash = password_hash($password, self::HASH, ['cost' => self::COST]); } // Logic for logging a user in: public function login($password) { // First see if they gave the right password: echo "Login: ", $this->data->passwordHash, "/n"; if (password_verify($password, $this->data->passwordHash)) { // Success - Now see if their password needs rehashed if (password_needs_rehash($this->data->passwordHash, self::HASH, ['cost' => self::COST])) { // We need to rehash the password, and save it. Just call setPassword $this->setPassword($password); $this->save(); } return true; // Or do what you need to mark the user as logged in. } return false; } }
回页首
现在您知道新的密码哈希库如何为 PHP 所用,以及它如何继续帮助您的用户防御安全破坏。在下一期 PHP 的复兴 中,我会将讨论主题转向 PHP 语言本身向生态系统的演变,以及已经开始围绕它进行演变的工具:首先是 Composer,一个风靡整个社区的 PHP 依赖项管理器。