choks/password-policy-bundle

v2.0 2024-03-01 20:18 UTC

This package is auto-updated.

Last update: 2024-09-30 21:48:14 UTC


README

CI

这是什么?

密码策略是一个 Symfony 扩展包,可以用来验证用户密码是否符合策略。

先决条件

  • PHP: >=8.1
  • OpenSSL PHP 扩展
  • Symfony 6 或 7

安装

通过 composer 安装

composer require choks/password-policy-bundle

添加到您的扩展包

Choks\PasswordPolicy\PasswordPolicy::class => ['all' => true],

如果您使用 doctrine 和 DBAL 存储时,在生成模式时,将自动安装用于存储密码历史的表。如果不使用,您需要 手动创建它

用法

在深入探讨之前,让我们解释一下它是如何工作的。首先,只要对象实现了 Choks\PasswordPolicy\Contract\PasswordPolicySubjectInterface,就可以在该对象上使用策略进行验证,这将需要实现 getIdentifier() 以便我们在密码历史记录中区分密码的所有者,以及 getPlainPassword() 以便我们可以进行比较(为什么是明文密码?)

基本上有两种验证方式

  • 手动,通过调用服务方法
  • 自动,通过在 Doctrine 实体上放置 #[Choks\PasswordPolicy\Atrribute\Listen] 属性

以及在您的应用程序中指定策略的两种方式

  • 通过扩展包配置(或)
  • 通过您自己的策略提供者

定义策略

通过配置

password_policy:
  policy:
    expiration:
      expires_after:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1
    character:
      min_length: 8 # Minimum password length, leave null if you don't want to use
      numbers: 1 # At least how many numbers there? Leave null if you don't want to use
      lowercase: 1 # At least how many lowercase characters there? Leave null if you don't want to use
      uppercase: 1 # At least how many uppercase characters there?Leave null if you don't want to use
      special: 1 # At least how many special characters there? Leave null if you don't want to use
    # Password history policy is used when you want your passwords to be validated against previous passwords.
    # By default, History Policy is not used.
    history:
      # Provided password should not be used in 10 previous passwords. Leave null if you don't want to use
      not_used_in_past_n_passwords: 10
      # Period for which we should look in the past. Leave null if you don't want to use
      period:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1

就这样,您的配置已经设置好了

通过您自己的策略提供者

以下是自定义策略提供者的一个示例。

use Choks\PasswordPolicy\Contract\PolicyInterface;
use Choks\PasswordPolicy\Contract\PolicyProviderInterface;
use Choks\PasswordPolicy\Model\ExpirationPolicy;
use Choks\PasswordPolicy\Model\CharacterPolicy;
use Choks\PasswordPolicy\Model\HistoryPolicy;
use Choks\PasswordPolicy\Model\Policy;

final class MyCustomPolicyProvider implements PolicyProviderInterface
{
    public function getPolicy(UserInterface $user): PolicyInterface
    {
        // Assuming that you have your own way of storing Policy configuration, for example db.
        $policyData = $this->entityManager->getRepository()->yourOwnWayOfFetchingData();

        return new Policy(
            new ExpirationPolicy(/* Use your stored data to construct */),
            new CharacterPolicy(/* Use your stored data to construct */),
            new HistoryPolicy(/* Use your stored data to construct */),
        );
    }
}

下一步是将它注册为服务,然后将其放入扩展包配置

password_policy:
  policy_provider: MyCustomPolicyProvider::class # You can put your own provider here

无论您是手动验证还是自动验证,此提供者都会被调用来获取要使用的策略。

检查策略和操作密码历史记录

手动

检查策略

您可以通过 ID password_policy.checkerChoks\PasswordPolicy\Contract\PolicyCheckerInterface 获取或注入检查器服务。假设您正在验证 $user(请记住,$user 必须实现 PasswordPolicySubjectInterface

$checker = $this->getContainer()->get('password_policy.checker')
$violations = $checker->validate($user);

if ($violations->hasErrors()) {
    // Do own stuff, you have violations to check error messages.
}

添加到密码历史记录(如果您使用它)

您可以通过 ID password_policy.historyChoks\PasswordPolicy\Contract\PasswordHistoryInterface 获取或注入密码历史记录服务

$history = $this->getContainer()->get('password_policy.history')
$history->add($user); // This will write password into password history.
# ...
# also:

$history->clear(); // This will clear all passwords in history, for all users.
$history->remove($user); // This will clear all passwords in history, for specific User.

自动检查

尽管我鼓励您手动控制检查流程,但让扩展包为您自动执行会更容易。通过添加 #[Listen] 属性,您期望扩展包自动

  • 当用户被插入或更新(当 flush 发生时)
    • 使用 getPlainPassword() 获取的密码与当前策略进行验证,
    • 如果使用历史策略,将密码添加到历史记录中(使用 crypt,请参阅以下完整配置参考)
  • 当用户被删除(当 flush 发生时)
    • 从历史记录中删除用户的密码。
use Choks\PasswordPolicy\Contract\PasswordPolicySubjectInterface;
use Doctrine\ORM\Mapping as ORM;
use Choks\PasswordPolicy\Atrribute\PasswordPolicy;

#[PasswordPolicy]
#[ORM\Entity]
class User implements PasswordPolicySubjectInterface
{
    #[ORM\Id]
    #[ORM\Column]
    public int $id;
   
    public ?string $plainPassword = null;

    public function __construct(int $id, string $plainPassword = null)
    {
        $this->id            = $id;
        $this->plainPassword = $plainPassword;
    }

    public function getIdentifier(): string
    {
        return (string)$this->id;
    }

    public function setPlainPassword(#[\SensitiveParameter] ?string $plainPassword): User
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }
    
    public function getPlainPassword(): ?string
    {
        return $this->plainPassword;
    }
}

在保存实体时,如果验证失败,将抛出 Choks\PasswordPolicy\Exception\PolicyCheckException。如果您捕获它,您可以通过 getViolations() 检查违规行为,

清除所有密码历史记录

在某些情况下,您可能想要从历史记录中清除所有密码。可能是在更新此扩展包之后,或者当您更改策略时。您可以通过执行一个命令来完成此操作

bin/console password-policy:history:clear

过期

您还可以设置过期策略。如果您想获取过期的密码,可以使用

  $expirationService = $this->getContainer()->get('password_policy.expiration');
  $password = $expirationService->getExpired($user); // Returns Choks\PasswordPolicy\ValueObject\Password

如果您想在服务中使用它,请使用自动装配注入 Choks\PasswordPolicy\Contract\PasswordExpirationInterface。为了进行更多定制,您还可以调用 processExpired 方法,并将 Choks\PasswordPolicy\Event\ExpiredPasswordEvent 触发,这样您就可以监听并捕获它,如果最后密码被发现已过期

$password = $expirationService->processExpired($user);

为什么是明文密码?这是安全的吗?

在使用Symfony的password_hashers算法时,可能并且通常是不可预测的。这意味着对于相同的明文密码,每个散列值都不同。此外,通常这些散列算法是单向的,意味着它们不能被解密或反散列。

哈希器也不能让我们将用户的明文密码与某个散列值进行比较,它只能使用UserPasswordHasherInterface在用户上验证散列密码,与明文密码进行比较。

我们需要在历史记录中存储加密的密码,为此,包使用它自己的加密算法(这就是为什么你会在配置中看到cipher_method。你始终可以选择不同的一个)。当我们比较用户明文密码和历史记录中的密码时,我们会解密这些密码并进行比较。

如何通过getPlainPassword()传递明文密码取决于你,但我鼓励你不要持久化它,如果可以的话,使用eraseCredentials()来取消设置它。

注意:如果getPlainPassword()返回NULL,将跳过所有密码策略操作。

配置参考

password_policy:
  policy_provider: ConfigurationPolicyProvider::class # You can put your own provider here
  special_chars: "\"'!@#$%^&*()_+=-`~.,;:<>[]{}\\|" # Which characters are considered special chars
  trim: true # Should we trim given password?
  salt: '%env(APP_SECRET)%' # Salt used when encrypting passwords
  cipher_method: aes-128-ctr # Check https://php.ac.cn/manual/en/function.openssl-get-cipher-methods.php

  # This policy is what would be used in your application as policy, if you don't specify your own provider
  policy:
    expiration:
      expires_after:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1
    character:
      min_length: 8 # Minimum password length (default is null)
      numbers: 1 # At least how many numbers there? (default is null)
      lowercase: 1 # At least how many lowercase characters there? (default is null)
      uppercase: 1 # At least how many uppercase characters there? (default is null)
      special: 1 # At least how many special characters there? (default is null)
    # Password history policy is used when you want your passwords to be validated against previous passwords.
    # By default, History Policy is not used.
    history:
      not_used_in_past_n_passwords: 10 # Provided password should not be used in 10 previous passwords.
      period: # Period for which we should look in the past. 
        unit: 'month' # Possible values are 'day', 'week', 'year' (default is null) 
        value: 1  (default is null)

  storage: # Only one storage can be defined. Storage is used to store password history
    dbal:
        table: 'password_history' # Name of the table where historic passwords should be stored.
        connection: 'default' # Doctrine DBAL connection name.
    cache:
      adapter: cache.app # your application cache adapter (see Symfony framework cache docs)
      key_prefix: 'password_history' # Prefix used for cache keys

注意:not_used_in_past_n_passwordsperiod可以单独使用或组合使用(一个设置,另一个不设置)。但为了使用周期,必须设置单位和值。

存储历史记录的表

如果你没有使用Doctrine生成模式,或者在某些情况下你的表没有创建,你可以通过这个DDL手动创建它:

CREATE TABLE password_history
(
    subject_id    VARCHAR(64)  NOT NULL,
    password      VARCHAR(128) NOT NULL,
    created_at    DATETIME     NOT NULL COMMENT '(DC2Type:datetime_immutable)'
)
    COLLATE = utf8mb4_unicode_ci;

CREATE INDEX IDX_F3521448B8E8428
    ON password_history (created_at);

CREATE INDEX IDX_F352144A76ED395
    ON password_history (subject_id);

未来计划做什么(不保证)?

  • 为实体定义自定义策略提供者,通过#[Listen]

贡献

随时欢迎贡献。请提供新的测试或更改后的测试。另外,如果你发现一些错误,请打开一个问题,我会尽快修复它。

待办事项

  • 历史记录中的密码垃圾回收(FILO,按用户)
  • 支持在没有Doctrine ORM和没有PostSchema监听器的情况下更新模式