跳到内容

密码哈希和验证

编辑此页

大多数应用程序使用密码来登录用户。这些密码应该被哈希以安全地存储它们。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();
        }
    }
}

重置密码

使用MakerBundleSymfonyCastsResetPasswordBundle,您可以创建一个安全的开箱即用解决方案来处理忘记密码的情况。首先,安装 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-ulidmake:reset-password。利用 Symfony 的 Uid 组件,实体将使用 id 类型生成为 UuidUlid 而不是 int

您可以通过更新 reset_password.yaml 文件来自定义重置密码 bundle 的行为。有关配置的更多信息,请查看 SymfonyCastsResetPasswordBundle 指南。

密码迁移

为了保护密码,建议使用最新的哈希算法存储它们。这意味着,如果您的系统支持更好的哈希算法,则应该使用较新的算法重新哈希用户的密码并存储。这可以通过 migrate_from 选项实现

  1. 使用 "migrate_from" 配置新的哈希器
  2. 升级密码
  3. 可选地,从自定义哈希器触发密码迁移

使用 "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 将使用旧算法验证密码,然后使用新算法重新哈希并更新密码。

提示

autonativebcryptargon 哈希器自动启用密码迁移,使用以下 migrate_from 算法列表

  1. PBKDF2(它使用 hash_pbkdf2);
  2. 消息摘要(它使用 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 哈希器。仍然使用它的遗留应用程序被鼓励升级到那些更新的哈希算法。

创建自定义密码哈希器

如果您需要创建自己的哈希器,则需要遵循以下规则

  1. 该类必须实现 PasswordHasherInterface(如果您的哈希算法使用单独的 salt,您也可以实现 LegacyPasswordHasherInterface);
  2. 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'
这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本