choks / password-policy-bundle
Requires
- php: >=8.1
- ext-openssl: *
- doctrine/orm: ^2.0|^3.0
- symfony/console: ^6.0|^7.0
- symfony/dependency-injection: ^6.0|^7.0
- symfony/framework-bundle: ^6.0|^7.0
- symfony/security-bundle: ^6.0|^7.0
- symfony/translation: ^6.0|^7.0
- symfony/yaml: ^6.0|^7.0
Requires (Dev)
This package is auto-updated.
Last update: 2024-09-30 21:48:14 UTC
README
这是什么?
密码策略是一个 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.checker
或 Choks\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.history
或 Choks\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_passwords
和period
可以单独使用或组合使用(一个设置,另一个不设置)。但为了使用周期,必须设置单位和值。
存储历史记录的表
如果你没有使用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监听器的情况下更新模式