stadly / password-police
简化密码策略执行。
Requires
- php: >=7.1
- http-interop/http-factory-discovery: ^1.4
- nesbot/carbon: ^2.9
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- stadly/php-date: ^1.0
- symfony/translation: ^4.3.3
- vanderlee/php-stable-sort-functions: ^2.0.4
Requires (Dev)
- jakub-onderka/php-parallel-lint: ^1.0
- pepakriz/phpstan-exception-rules: ^0.8.0
- phpstan/phpstan: ^0.11.10
- phpstan/phpstan-deprecation-rules: ^0.11.0
- phpstan/phpstan-phpunit: ^0.11.0
- phpstan/phpstan-strict-rules: ^0.11.0
- phpunit/phpunit: ^7.1
- stadly/php-style: ^1.0
Suggests
- ext-pspell: To use pspell with a dictionary.
- nyholm/psr7: Automatically discoverable HTTP request factory.
- php-http/guzzle6-adapter: Automatically discoverable HTTP client.
README
简化密码策略执行。
安装
通过 Composer
composer require stadly/password-police
使用
use Stadly\PasswordPolice\Policy; use Stadly\PasswordPolice\Rule\DictionaryRule; use Stadly\PasswordPolice\Rule\HaveIBeenPwnedRule; use Stadly\PasswordPolice\Rule\LengthRule; use Stadly\PasswordPolice\WordList\Pspell; $policy = new Policy(); // Add rules to the password policy. See the Rules section below. $policy->addRules( new LengthRule(8), // Password must be at least 8 characters long. new HaveIBeenPwnedRule(), // Password must not be exposed in data breaches. new DictionaryRule(Pspell::fromLocale('en')) // Password must not be a word from the dictionary. ); $password = 'password'; $validationErrors = $policy->validate($password); if ($validationErrors !== []) { // The password is not in compliance with the policy. foreach ($validationErrors as $validationError) { // Show validation message to the user. echo $validationError->getMessage(); } }
规则
密码策略规则指定密码要求,并用于判断密码是否符合策略。
长度
长度规则设置密码长度的上下限。
use Stadly\PasswordPolice\Rule\LengthRule; $rule = new LengthRule(8); // Password must be at least 8 characters long. $rule = new LengthRule(8, 32); // Password must be between 8 and 32 characters long.
小写字母
小写规则设置小写字母的数量上下限。
use Stadly\PasswordPolice\Rule\LowerCaseRule; $rule = new LowerCaseRule(); // Password must contain lower case letters. $rule = new LowerCaseRule(3); // Password must contain at least 3 lower case letters. $rule = new LowerCaseRule(3, 5); // Password must contain between 3 and 5 lower case letters.
大写字母
大写规则设置大写字母的数量上下限。
use Stadly\PasswordPolice\Rule\UpperCaseRule; $rule = new UpperCaseRule(); // Password must contain upper case letters. $rule = new UpperCaseRule(3); // Password must contain at least 3 upper case letters. $rule = new UpperCaseRule(3, 5); // Password must contain between 3 and 5 upper case letters.
数字
数字规则设置数字的数量上下限。
use Stadly\PasswordPolice\Rule\DigitRule; $rule = new DigitRule(); // Password must contain digits. $rule = new DigitRule(3); // Password must contain at least 3 digits. $rule = new DigitRule(3, 5); // Password must contain between 3 and 5 digits.
符号
符号规则设置符号的数量上下限。在创建规则时指定考虑为符号的字符。注意,此规则计算符号数量,而不是不同符号的数量。例如,Hello!!!
包含一个符号三次,而 Hello!?&
包含三个不同的符号,两者都包含三个符号。
use Stadly\PasswordPolice\Rule\SymbolRule; $rule = new SymbolRule('!#%&?'); // Password must contain symbols (!, #, %, &, or ?). $rule = new SymbolRule('!#%&?', 3); // Password must contain at least 3 symbols. $rule = new SymbolRule('!#%&?', 3, 5); // Password must contain between 3 and 5 symbols.
我已经泄露了吗?
“我已经泄露了吗?”规则设置密码在数据泄露中暴露次数的上下限。此规则使用 “我已经泄露了吗?” 服务。密码永远不会发送到该服务。相反,使用 k-Anonymity 使解决方案安全。
use Stadly\PasswordPolice\Rule\HaveIBeenPwnedRule; $rule = new HaveIBeenPwnedRule(); // Password must not be exposed in data breaches. $rule = new HaveIBeenPwnedRule(5); // Password must not be exposed in data breaches more than 5 times. $rule = new HaveIBeenPwnedRule(5, 3); // Password must be exposed in data breachs between 3 and 5 times.
字典
字典规则强制密码不包含在单词列表中。
格式化程序 可选地应用于检查单词列表之前的密码。这使得例如解码 leetspeak 成为可能,以便密码 p4ssw0rd
会与包含单词 password
的单词列表匹配,或者将密码 SomeCombinedWords
分割 成多个单词,以便密码 SomeCombinedWords
会与包含单词 Combined
的单词列表匹配。
use Stadly\PasswordPolice\Formatter\LeetspeakDecoder; use Stadly\PasswordPolice\Rule\DictionaryRule; use Stadly\PasswordPolice\WordList\Pspell; $wordList = Pspell::fromLocale('en'); $rule = new DictionaryRule($wordList, [new LeetspeakDecoder()]);
单词列表
字典规则需要单词列表。目前,Pspell 是唯一可用的单词列表。支持其他单词列表可以轻松实现。
Pspell
pspell 单词列表使用 Pspell,可以构建到 php 中。
格式化程序 可选地应用于检查单词列表之前的密码。这很有用,因为 pspell 的 php 版本是区分大小写的。通过使用例如 小写转换器,密码 PaSsWoRd
会与包含单词 password
的单词列表匹配。
use Stadly\PasswordPolice\Formatter\Capitalizer; use Stadly\PasswordPolice\Formatter\LowerCaseConverter; use Stadly\PasswordPolice\WordList\Pspell; $wordList = Pspell::fromLocale('en', [new LowerCaseConverter(), new Capitalizer()]);
可猜测的数据
猜测数据规则强制要求密码不包含容易猜测的数据。在创建规则时指定容易猜测的数据。可以为每个密码指定额外的容易猜测的数据。这样,猜测数据规则既可以防止像服务名称这样的通用容易猜测的数据被用于任何密码,也可以防止用户在密码中使用自己的姓名或生日等个人容易猜测的数据。
要为密码指定容易猜测的数据,密码必须是Password
对象,而不是string
。
格式化程序可以在验证之前可选地应用于密码。这使得解码例如 leetspeak 成为可能,因此密码 S74d1y
将与容易猜测的数据 Stadly
匹配。请注意,猜测数据规则检查密码是否包含容易猜测的数据,而不是它是否匹配,因此不需要使用格式化程序将密码拆分为 多个单词,这在字典规则中可能是有用的。
use Stadly\PasswordPolice\Formatter\LeetspeakDecoder; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Rule\GuessableDataRule; // Easily guessable data for any password. $globalGuessableData = [ 'company', ]; $rule = new GuessableDataRule($globalGuessableData, [new LeetspeakDecoder()]); // Additional easily guessable data for this password. $passwordGuessableData = [ 'first name', 'spouse', new DateTimeImmutable('birthday'), ]; // Use a Password object instead of a string in order to specify the easily guessable data. $password = new Password('password', $passwordGuessableData));
日期格式化程序
为了检查密码是否包含容易猜测的日期,猜测数据规则必须知道日期可以有的不同格式。这是日期格式化程序的工作。可以轻松实现自定义日期格式化程序。如果没有为猜测数据规则指定日期格式化程序,则使用默认日期格式化程序。
use Stadly\PasswordPolice\Rule\GuessableDataRule; $dateFormatter = new MyCustomDateFormatter(); // Easily guessable data. $guessableData = [ new DateTimeImmutable('2018-08-04'), ]; $rule = new GuessableDataRule($guessableData, [], $dateFormatter);
不重复使用
不重复使用规则防止以前使用的密码再次使用。
为了使规则知道以前使用的密码,必须指定以前的密码。要指定以前的密码,密码必须是Password
对象,而不是字符串。
use Stadly\PasswordPolice\FormerPassword; use Stadly\PasswordPolice\HashFunction\PasswordHasher; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Rule\NoReuseRule; $hashFunction = new PasswordHasher(); $rule = new NoReuseRule($hashFunction); // Passwords can never be reused. $rule = new NoReuseRule($hashFunction, 5); // The 5 most recently used password cannot be reused. // Former passwords. The most recent one should be the current password. $formerPasswords = [ new FormerPassword(new DateTimeImmutable('2017-06-24'), 'hash of password'), new FormerPassword(new DateTimeImmutable('2018-08-04'), 'hash of password'), new FormerPassword(new DateTimeImmutable('2018-08-18'), 'hash of password'), ]; // Use a Password object instead of a string in order to specify former passwords. $password = new Password('password', [], $formerPasswords));
哈希函数
密码应始终以安全哈希的形式存储,使其无法从其存储表示中确定原始密码。为了检查密码是否与以前使用的密码匹配,不重复使用规则必须使用与创建密码哈希相同的算法。这是哈希函数的工作,它允许比较原始密码和哈希密码。目前,只有一个可用的哈希函数,支持内置的PHP密码哈希算法。可以轻松实现其他哈希函数的支持。
密码哈希器
密码哈希器使用内置于PHP中的密码哈希函数。如果您使用password_hash
来存储密码的哈希值,这是不重复使用规则的正确选择。
按间隔更改
按间隔更改规则为密码更改的频率设置下限和上限。从一次密码更改到下一次更改的时间的下限可以防止密码更改过于频繁。这可以与强制执行例如5个最近密码不能重复使用的不重复使用规则结合使用,因为它防止用户只是更改密码5次然后回到原始密码。从一次密码更改到下一次更改的时间的上限强制执行定期更改密码。
为了使规则知道密码何时更改,必须指定以前的密码。要指定以前的密码,密码必须是Password
对象,而不是字符串。
use Stadly\PasswordPolice\FormerPassword; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Rule\ChangeWithIntervalRule; // There must be at least 24 hours between password changes. $rule = new ChangeWithIntervalRule(new DateInterval('PT24H')); // There must be at most 30 days between password changes. $rule = new ChangeWithIntervalRule(new DateInterval('PT0S'), new DateInterval('P30D')); // Former passwords. The most recent one should be the current password. $formerPasswords = [ new FormerPassword(new DateTimeImmutable('2017-06-24')), new FormerPassword(new DateTimeImmutable('2018-08-04')), new FormerPassword(new DateTimeImmutable('2018-08-18')), ]; // Use a Password object instead of a string in order to specify former passwords. $password = new Password('password', [], $formerPasswords));
在间隔内未设置
未设置时间段规则的目的是确保密码在指定时间段内未设置。例如,在数据泄露事件后,所有密码都应更改,或对于其他安全事件,只需更改安全事件期间设置的密码。
为了使规则知道密码何时设置,必须将当前密码指定为旧密码。要指定旧密码,密码必须是Password
对象而不是字符串。
use Stadly\PasswordPolice\FormerPassword; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Rule\NotSetInIntervalRule; // Password must have been set after 2019-02-10. $rule = new NotSetInIntervalRule(new DateTimeImmutable('2019-02-10')); // Password cannot have been set between 2019-02-10 and 2019-02-13. $rule = new NotSetInIntervalRule(new DateTimeImmutable('2019-02-13'), new DateTimeImmutable('2019-02-10')); // Former passwords. The most recent one should be the current password. $formerPasswords = [ new FormerPassword(new DateTimeImmutable('2017-06-24')), new FormerPassword(new DateTimeImmutable('2018-08-04')), new FormerPassword(new DateTimeImmutable('2018-08-18')), ]; // Use a Password object instead of a string in order to specify former passwords. $password = new Password('password', [], $formerPasswords));
条件规则
条件规则用于有条件地应用另一个规则。必须指定要条件应用的条件规则和条件函数。条件函数应仅接受密码(无论是string
还是Password
对象)作为参数,并返回true
或false
。如果条件函数返回false,则此规则不执行任何操作。否则,应用指定的规则。例如,这可以用于定期应用我已经被破解了吗规则。例如,每月最多检查一次密码是否包含在数据泄露中,而不是每次使用密码时都检查。
use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Rule\ConditionalRule; /** * @param Password|string $password * @return bool */ $conditionFunction = function($password): bool { return true; // Whether the rule should be applied. } $rule = new ConditionalRule($ruleToApplyConditionally, $conditionFunction);
格式化工具
格式化工具用于操作字符串,可以与字典和可猜测数据规则以及pspell词表结合使用。格式化工具可以链式使用,即一个格式化工具的结果被输入到另一个格式化工具(格式化工具按顺序运行)。格式化工具也可以组合使用,即多个格式化工具的结果合并为一个(格式化工具并行运行)。
转换器
转换器格式化工具可以将字符串中的字符转换为其他字符。
大写转换器
大写转换器将第一个字符转换为大写,其余转换为小写。
use Stadly\PasswordPolice\Formatter\Capitalizer; $formatter = new Capitalizer();
Leetspeak解码器
Leetspeak解码器解码可以解释为Leetspeak的字符序列。结果包含所有解码组合,因此格式化字符串1337
会产生字符串1337
、L337
、1E37
、13E7
、133T
、LE37
、L3E7
、L33T
、1EE7
、1E3T
、13ET
、LEE7
、LE3T
、L3ET
、1EET
、LEET
。
use Stadly\PasswordPolice\Formatter\LeetspeakDecoder; $formatter = new LeetspeakDecoder();
小写转换器
小写转换器将所有字符转换为小写。
use Stadly\PasswordPolice\Formatter\LowerCaseConverter; $formatter = new LowerCaseConverter();
混合大小写转换器
混合大小写转换器将所有字符转换为大小写。结果包含所有组合,因此格式化字符串fOo
会产生字符串foo
、Foo
、fOo
、foO
、FOo
、FoO
、fOO
、FOO
。
use Stadly\PasswordPolice\Formatter\MixedCaseConverter; $formatter = new MixedCaseConverter();
大写转换器
大写转换器将所有字符转换为大写。
use Stadly\PasswordPolice\Formatter\UpperCaseConverter; $formatter = new UpperCaseConverter();
分割器
分割器格式化工具可以提取字符串的一部分。
子字符串生成器
子字符串生成器生成所有子字符串。可以指定子字符串的最小和最大长度。小于最小长度或大于最大长度的子字符串不包括在结果中。结果仅包括唯一的字符串。
use Stadly\PasswordPolice\Formatter\SubstringGenerator; // Ignore substring shorter than 3 characters or longer than 25 character. $formatter = new SubstringGenerator(3, 25);
截断器
截断器将字符串截断到最大长度。
use Stadly\PasswordPolice\Formatter\Truncator; $formatter = new Truncator(25); // Truncate strings so they contain no more than 25 characters.
过滤器
过滤器格式化工具可以过滤掉某些字符串。
长度过滤器
长度过滤器过滤掉长度短于或长于限制的字符串。
use Stadly\PasswordPolice\Formatter\LengthFilter; $formatter = new LengthFilter(3); // Filter out strings shorter than 3 characters. $formatter = new LengthFilter(0, 25); // Filter out strings longer than 25 characters. $formatter = new LengthFilter(3, 25); // Filter out strings shorter than 3 or longer than 25 characters.
组合器
格式化合并器将多个格式化器的结果合并成一个(格式化器并行运行)。默认情况下,未格式化的字符串也会包含在结果中,但可以排除。结果只包含唯一的字符串。
use Stadly\PasswordPolice\Formatter\Combiner; use Stadly\PasswordPolice\Formatter\LowerCaseConverter; use Stadly\PasswordPolice\Formatter\UpperCaseConverter; $lower = new LowerCaseConverter(); $upper = new UpperCaseConverter(); $formatter = new Combiner($lower, $upper); // Lower case, upper case and unformatted strings. $formatter = new Combiner($lower, $upper, false); // Lower case and upper case strings.
链式调用
格式化器可以链式调用,以便将一个格式化器的结果传递给另一个格式化器(格式化器按顺序运行)。
use Stadly\PasswordPolice\Formatter\LeetspeakDecoder; use Stadly\PasswordPolice\Formatter\SubstringGenerator; $formatter = new LeetspeakDecoder(); // First decode leetspeak, and then generate all substrings. $formatter->setNext(new SubstringGenerator());
规则权重
所有规则都关联一个权重。默认权重为1。通过使用权重,可以区分不能规避的硬规则和仅作为建议的规则,这些规则可能被忽略。
测试规则或策略时的规则权重
在测试规则或策略时,可以指定一个可选的权重。权重低于测试权重的规则将被忽略。
use Stadly\PasswordPolice\Policy; use Stadly\PasswordPolice\Rule\DigitRule; use Stadly\PasswordPolice\Rule\LengthRule; $policy = new Policy(); $policy->addRules(new LengthRule(8, null, 1)); // Rule weight: 1. $policy->addRules(new DigitRule(1, null, 2)); // Rule weight: 2. $password = '123'; $policy->test($password, 1); // False, since the password is too short. $policy->test($password, 2); // True, since the length rule is ignored.
验证规则或策略时的规则权重
在验证规则或策略时,无论权重如何,所有规则都将被验证。每个验证错误都包含破坏规则的权重,可用于忽略低权重的验证错误。
use Stadly\PasswordPolice\Policy; use Stadly\PasswordPolice\Rule\DigitRule; use Stadly\PasswordPolice\Rule\LengthRule; $policy = new Policy(); $policy->addRules(new LengthRule(8, null, 1)); // Rule weight: 1. $policy->addRules(new DigitRule(1, null, 2)); // Rule weight: 2. $password = '123'; $validationErrors = $policy->validate($password); foreach ($validationErrors as $validationError) { // Ignore validation errors of weight lower than or equal to 1. if ($validationError->getWeight() > 1) { // Show validation message to the user. echo $validationError->getMessage(); } }
约束
除了为不同规则指定不同的权重外,大多数规则还可以包含多个具有不同权重的约束。这使得可以创建具有严格约束且权重低的规则,以及具有较宽松约束且权重较高的规则。
测试规则或策略时的约束权重
在测试规则或策略时,可以指定一个可选的权重。权重低于测试权重的规则约束将被忽略。
use Stadly\PasswordPolice\Rule\LengthRule; $rule = new LengthRule(12, null, 1); // Constraint weight: 1. $rule->addConstraint(8, null, 2); // Constraint weight: 2. $password = 'password'; $rule->test($password, 1); // False, since the password is too short. $rule->test($password, 2); // True, since the strict constraint is ignored.
验证规则或策略时的约束权重
在验证规则或策略时,无论权重如何,所有规则约束都将被验证。每个验证错误都包含不满足的规则约束的权重,可用于忽略低权重的验证错误。
use Stadly\PasswordPolice\Rule\LengthRule; $rule = new LengthRule(12, null, 1); // Constraint weight: 1. $rule->addConstraint(8, null, 2); // Constraint weight: 2. $password = 'password'; $validationErrors = $policy->validate($password); foreach ($validationErrors as $validationError) { // Ignore validation errors of weight lower than or equal to 1. if ($validationError->getWeight() > 1) { // Show validation messages to the user. echo $validationError->getMessage(); } }
权重示例用法
在Have I Been Pwned服务的第2版中,引入了密码在数据泄露中出现的次数。当撰写关于新版本的文章时,Troy Hunt给出了一例如何利用这个数字
对普遍性的了解意味着,例如,可以完全阻止出现100次或更多次的密码,并强制用户选择另一个(数据集中有1,858,690个这样的密码),强烈建议在出现20到99次时选择不同的密码(有进一步9,985,150个这样的密码),如果来源数据中出现次数少于20次,则仅标记记录。
可以通过创建3个具有不同权重的规则约束来实现此类密码策略
use Stadly\PasswordPolice\Rule\HaveIBeenPwnedRule; // Weight 1 when password has appeared in data breaches 100 times or more. $rule = new HaveIBeenPwnedRule(99, 0, 1); // Weight 0 when password has appeared in data breaches between 20 and 99 times. $rule->addConstraint(19, 0, 0); // Weight -1 when password has appeared in data breaches between 1 and 19 times. $rule->addConstraint(0, 0, -1);
在验证密码时,可以使用验证错误的权重来确定采取哪种操作
$validationErrors = $policy->validate($password); foreach ($validationErrors as $validationError) { if ($validationError->getWeight() === 1) { // Reject the password. } elseif ($validationError->getWeight() === 0) { // Recommend choosing a different password. } elseif ($validationError->getWeight() === -1) { // Flag the password. } }
密码策略的最佳实践
良好密码策略的一般指南
- 要求密码至少为8个字符长。
- 不允许在数据泄露中暴露的密码。
- 不允许在字典中找到的密码。
- 不允许包含易猜的单词的密码,例如服务名称或用户名称。
- 不允许包含重复或顺序字符的密码(例如“aaaaaa”,“1234”,“abcd”或“qwerty”)。
- 不需要同时包含以下内容:小写字母、大写字母、数字和符号。
- 不需要密码定期更改。
您可以在NIST SP 800-63B的第5.1.1节和附录A中了解更多有关密码策略建议的信息。
何时验证密码
在两种情况下可以对密码进行验证,以检查是否符合密码策略。
- 当设置密码时。
- 当使用密码时。
密码应始终以安全散列的形式存储,使其无法从存储表示形式中确定原始密码。因此,原始密码仅在设置或使用时才可用,因此只能在那时进行密码验证。唯一的例外是不需要密码的密码策略规则,例如按间隔更改规则和在间隔内未设置规则。
建议在设置和使用密码时都进行密码验证,但两种情况下应用规则通常不同。
验证正在设置的密码
设置密码时是验证密码格式(如长度、小写字母、大写字母、数字和符号)的正确时间。此外,应使用Have I Been Pwned、字典和可猜测数据等规则验证密码内容。还应检查考虑以前密码的规则,例如不重复使用和带有下限的按间隔更改。
验证正在使用的密码
使用密码时,无需验证密码的格式。假设密码策略没有更改,如果密码在设置时通过了验证,则在使用时也会通过验证。当密码被使用时,只需对验证结果可能改变,而无需更改密码的规则进行验证。例如,这些规则包括Have I Been Pwned(以前有效的密码在新的数据泄露后可能变得无效)、带有上限的按间隔更改和在间隔内未设置。
更改日志
有关最近更改的更多信息,请参阅CHANGELOG。
测试
composer test
贡献
有关详细信息,请参阅CONTRIBUTING和CODE_OF_CONDUCT。
安全
如果您发现任何安全相关的问题,请通过电子邮件magnar@myrtveit.com联系,而不是使用问题跟踪器。
致谢
许可
MIT许可证(MIT)。请参阅许可证文件获取更多信息。