密码哈希和验证
大多数应用程序使用密码来登录用户。这些密码应该被哈希以安全地存储它们。Symfony 的 PasswordHasher 组件提供了安全地哈希和验证密码的所有实用程序。
请确保通过运行以下命令安装它
1
$ composer require symfony/password-hasher
配置密码哈希器
在哈希密码之前,您必须使用 password_hashers
选项配置一个哈希器。您必须配置哈希算法和可选的一些算法选项
1 2 3 4 5 6 7 8 9 10 11 12
# config/packages/security.yaml
security:
# ...
password_hashers:
# auto hasher with default options for the User class (and children)
App\Entity\User: 'auto'
# auto hasher with custom options for all PasswordAuthenticatedUserInterface instances
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: 'auto'
cost: 15
在这个例子中,使用了 "auto" 算法。这个哈希器自动选择您的系统上可用的最安全的算法。结合密码迁移,这允许您始终以最安全的方式保护密码(即使在未来的 PHP 版本中引入新的算法时也是如此)。
在本文的后面,您可以找到所有支持算法的完整参考。
提示
哈希密码是资源密集型的,并且需要时间来生成安全的密码哈希值。一般来说,这使您的密码哈希更加安全。
然而,在测试中,安全的哈希并不重要,因此您可以在 test
环境中更改密码哈希器配置以更快地运行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/test/security.yaml
security:
# ...
password_hashers:
# Use your user class name here
App\Entity\User:
algorithm: plaintext # disable hashing (only do this in tests!)
# or use the lowest possible values
App\Entity\User:
algorithm: auto # This should be the same value as in config/packages/security.yaml
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
哈希密码
配置正确的算法后,您可以使用 UserPasswordHasherInterface
来哈希和验证密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// src/Controller/RegistrationController.php
namespace App\Controller;
// ...
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserController extends AbstractController
{
public function registration(UserPasswordHasherInterface $passwordHasher): Response
{
// ... e.g. get the user data from a registration form
$user = new User(...);
$plaintextPassword = ...;
// hash the password (based on the security.yaml config for the $user class)
$hashedPassword = $passwordHasher->hashPassword(
$user,
$plaintextPassword
);
$user->setPassword($hashedPassword);
// ...
}
public function delete(UserPasswordHasherInterface $passwordHasher, UserInterface $user): void
{
// ... e.g. get the password from a "confirm deletion" dialog
$plaintextPassword = ...;
if (!$passwordHasher->isPasswordValid($user, $plaintextPassword)) {
throw new AccessDeniedHttpException();
}
}
}
重置密码
使用MakerBundle 和 SymfonyCastsResetPasswordBundle,您可以创建一个安全的开箱即用解决方案来处理忘记密码的情况。首先,安装 SymfonyCastsResetPasswordBundle
1
$ composer require symfonycasts/reset-password-bundle
然后,使用 make:reset-password
命令。这将询问您一些关于您的应用程序的问题,并生成您需要的所有文件!之后,您将看到一条成功消息以及您需要执行的任何其他步骤的列表。
1
$ php bin/console make:reset-password
提示
从 MakerBundle: v1.57.0 开始 - 您可以传递 --with-uuid
或 --with-ulid
给 make:reset-password
。利用 Symfony 的 Uid 组件,实体将使用 id
类型生成为 Uuid 或 Ulid 而不是 int
。
您可以通过更新 reset_password.yaml
文件来自定义重置密码 bundle 的行为。有关配置的更多信息,请查看 SymfonyCastsResetPasswordBundle 指南。
密码迁移
为了保护密码,建议使用最新的哈希算法存储它们。这意味着,如果您的系统支持更好的哈希算法,则应该使用较新的算法重新哈希用户的密码并存储。这可以通过 migrate_from
选项实现
使用 "migrate_from" 配置新的哈希器
当更好的哈希算法可用时,您应该保留现有的哈希器,重命名它,然后定义新的哈希器。在新哈希器上设置 migrate_from
选项以指向旧的、遗留的哈希器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# config/packages/security.yaml
security:
# ...
password_hashers:
# a hasher used in the past for some users
legacy:
algorithm: sha256
encode_as_base64: false
iterations: 1
App\Entity\User:
# the new hasher, along with its options
algorithm: sodium
migrate_from:
- bcrypt # uses the "bcrypt" hasher with the default options
- legacy # uses the "legacy" hasher configured above
通过此设置
- 新用户将使用新算法进行哈希;
- 每当使用旧算法存储密码的用户登录时,Symfony 将使用旧算法验证密码,然后使用新算法重新哈希并更新密码。
提示
auto、native、bcrypt 和 argon 哈希器自动启用密码迁移,使用以下 migrate_from
算法列表
- PBKDF2(它使用 hash_pbkdf2);
- 消息摘要(它使用 hash)
两者都使用 hash_algorithm
设置作为算法。建议使用 migrate_from
而不是 hash_algorithm
,除非使用 auto 哈希器。
升级密码
成功登录后,安全系统会检查是否有更好的算法可用于哈希用户的密码。如果有,它将使用新的哈希值哈希正确的密码。当使用自定义身份验证器时,您必须在安全通行证中使用 PasswordCredentials
。
您可以通过实现如何存储这个新哈希的密码来启用升级行为
在此之后,您就完成了,密码始终尽可能安全地哈希!
注意
当在 Symfony 应用程序之外使用 PasswordHasher 组件时,您必须手动使用 PasswordHasherInterface::needsRehash()
方法来检查是否需要重新哈希,并使用 PasswordHasherInterface::hash()
方法来使用新算法重新哈希明文密码。
当使用 Doctrine 时升级密码
当使用实体用户提供器时,在 UserRepository
中实现 PasswordUpgraderInterface(有关如何创建此类(如果尚未创建),请参阅 Doctrine 文档)。此接口实现了存储新创建的密码哈希值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Repository/UserRepository.php
namespace App\Repository;
// ...
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
class UserRepository extends EntityRepository implements PasswordUpgraderInterface
{
// ...
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
// set the new hashed password on the User object
$user->setPassword($newHashedPassword);
// execute the queries on the database
$this->getEntityManager()->flush();
}
}
当使用自定义用户提供器时升级密码
如果您正在使用自定义用户提供器,请在用户提供器中实现 PasswordUpgraderInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Security/UserProvider.php
namespace App\Security;
// ...
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
// ...
public function upgradePassword(UserInterface $user, string $newHashedPassword): void
{
// set the new hashed password on the User object
$user->setPassword($newHashedPassword);
// ... store the new password
}
}
从自定义哈希器触发密码迁移
如果您正在使用自定义密码哈希器,您可以通过在 needsRehash()
方法中返回 true
来触发密码迁移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Security/CustomPasswordHasher.php
namespace App\Security;
// ...
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class CustomPasswordHasher implements PasswordHasherInterface
{
// ...
public function needsRehash(string $hashedPassword): bool
{
// check whether the current password is hashed using an outdated hasher
$hashIsOutdated = ...;
return $hashIsOutdated;
}
}
动态密码哈希器
通常,相同的密码哈希器用于所有用户,方法是将其配置为应用于特定类的所有实例。另一种选择是使用“命名”哈希器,然后动态选择您想要使用的哈希器。
默认情况下(如本文开头所示),auto
算法用于 App\Entity\User
。
对于普通用户来说,这可能足够安全,但是如果您希望您的管理员拥有更强的算法,例如具有更高成本的 auto
怎么办?这可以使用命名哈希器来完成
1 2 3 4 5 6 7
# config/packages/security.yaml
security:
# ...
password_hashers:
harsh:
algorithm: auto
cost: 15
这将创建一个名为 harsh
的哈希器。为了使 User
实例使用它,该类必须实现 PasswordHasherAwareInterface。该接口需要一个方法 - getPasswordHasherName()
- 它应该返回要使用的哈希器的名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// src/Entity/User.php
namespace App\Entity;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements
UserInterface,
PasswordAuthenticatedUserInterface,
PasswordHasherAwareInterface
{
// ...
public function getPasswordHasherName(): ?string
{
if ($this->isAdmin()) {
return 'harsh';
}
return null; // use the default hasher
}
}
警告
当迁移密码时,您不需要实现 PasswordHasherAwareInterface
来返回遗留哈希器名称:Symfony 将从您的 migrate_from
配置中检测到它。
如果您创建了自己的密码哈希器,实现了 PasswordHasherInterface,您必须为其注册一个服务,以便将其用作命名哈希器
1 2 3 4 5 6
# config/packages/security.yaml
security:
# ...
password_hashers:
app_hasher:
id: 'App\Security\Hasher\MyCustomPasswordHasher'
这将从 ID 为 App\Security\Hasher\MyCustomPasswordHasher
的服务创建一个名为 app_hasher
的哈希器。
哈希一个独立的字符串
密码哈希器可以用于独立于用户哈希字符串。通过使用 PasswordHasherFactory,您可以声明多个哈希器,使用其名称检索任何哈希器并创建哈希值。然后,您可以验证字符串是否与给定的哈希值匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
// configure different hashers via the factory
$factory = new PasswordHasherFactory([
'common' => ['algorithm' => 'bcrypt'],
'sodium' => ['algorithm' => 'sodium'],
]);
// retrieve the hasher using bcrypt
$hasher = $factory->getPasswordHasher('common');
$hash = $hasher->hash('plain');
// verify that a given string matches the hash calculated above
$hasher->verify($hash, 'invalid'); // false
$hasher->verify($hash, 'plain'); // true
支持的算法
"auto" 哈希器
它自动选择最佳可用哈希器(目前为 Bcrypt)。如果 PHP 或 Symfony 在未来添加了新的密码哈希器,它可能会选择不同的哈希器。
因此,哈希密码的长度将来可能会更改,因此请确保为它们分配足够的空间以进行持久化(varchar(255)
应该是一个不错的设置)。
Bcrypt 密码哈希器
它使用 bcrypt 密码哈希函数生成哈希密码。哈希密码的长度为 60
个字符,因此请确保为它们分配足够的空间以进行持久化。此外,密码还包含加密盐在其中(它为每个新密码自动生成),因此您不必处理它。
它唯一的配置选项是 cost
,它是一个介于 4-31
之间的整数(默认为 13
)。成本每增加一个单位,哈希密码所需的时间就会加倍。它被设计成这样,以便密码强度可以适应未来计算能力的提高。
您可以随时更改成本——即使您已经有一些使用不同成本哈希的密码。新密码将使用新成本进行哈希,而已经哈希的密码将使用它们哈希时使用的成本进行验证。
提示
当使用 BCrypt 时,使测试更快的一种简单技术是将成本设置为 4
,这是 test
环境配置中允许的最小值。
Sodium 密码哈希器
它使用 Argon2 密钥派生函数。Argon2 支持在 PHP 7.2 中通过捆绑 libsodium 扩展引入。
哈希密码的长度为 96
个字符,但由于保存在结果哈希值中的哈希要求,将来可能会更改,因此请确保为它们分配足够的空间以进行持久化。此外,密码还包含加密盐在其中(它为每个新密码自动生成),因此您不必处理它。
PBKDF2 哈希器
由于 PHP 添加了对 Sodium 和 BCrypt 的支持,因此不再建议使用 PBKDF2 哈希器。仍然使用它的遗留应用程序被鼓励升级到那些更新的哈希算法。
创建自定义密码哈希器
如果您需要创建自己的哈希器,则需要遵循以下规则
- 该类必须实现 PasswordHasherInterface(如果您的哈希算法使用单独的 salt,您也可以实现 LegacyPasswordHasherInterface);
hash() 和 verify() 的实现必须验证密码长度不超过 4096 个字符。 这是出于安全原因(请参阅 CVE-2013-5750)。
您可以使用 isPasswordTooLong() 方法进行此检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
// src/Security/Hasher/CustomVerySecureHasher.php
namespace App\Security\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class CustomVerySecureHasher implements PasswordHasherInterface
{
use CheckPasswordLengthTrait;
public function hash(string $plainPassword): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
// ... hash the plain password in a secure way
return $hashedPassword;
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
return false;
}
// ... validate if the password equals the user's password in a secure way
return $passwordIsValid;
}
public function needsRehash(string $hashedPassword): bool
{
// Check if a password hash would benefit from rehashing
return $needsRehash;
}
}
现在,使用 id
设置定义一个密码哈希器
1 2 3 4 5 6 7
# config/packages/security.yaml
security:
# ...
password_hashers:
app_hasher:
# the service ID of your custom hasher (the FQCN using the default services.yaml)
id: 'App\Security\Hasher\MyCustomPasswordHasher'