1. 简介

如今,大多数 Web 应用都有自己的密码策略,目的很简单:强制用户创建难以被破解的密码。

为了生成或验证这类密码,我们可以借助 Passay 库来实现。

2. Maven 依赖

如果要在项目中使用 Passay,需要在 pom.xml 文件中添加如下依赖:

<dependency>
    <groupId>org.passay</groupId>
    <artifactId>passay</artifactId>
    <version>1.3.1</version>
</dependency>

你可以在 Maven Central 找到该依赖。

3. 密码校验

密码校验是 Passay 提供的两大核心功能之一,使用起来非常直观和方便。我们一起来看看它是怎么工作的。

3.1. PasswordData

要进行密码校验,我们需要使用 PasswordData 对象。✅ 它是一个容器,用来存储校验所需的各类信息,包括:

  • 密码本身
  • 用户名
  • 密码引用列表(如历史密码、来源密码等)
  • 密码来源(是否由用户自定义或系统生成)

Passay 提供了 HistoricalReferenceSourceReference 来支持密码引用功能。

3.2. PasswordValidator

✅ 要开始校验密码,必须有 PasswordDataPasswordValidator 两个对象。我们已经介绍了 PasswordData,现在来看 PasswordValidator 的创建方式。

首先,我们需要定义一组校验规则,并将其传递给 PasswordValidator 的构造函数:

PasswordValidator passwordValidator = new PasswordValidator(new LengthRule(5));

然后,我们可以将密码传入 PasswordData,支持两种方式:

PasswordData passwordData = new PasswordData("1234");

PasswordData passwordData2 = new PasswordData();
passwordData.setPassword("1234");

最后,调用 validate() 方法进行校验:

RuleResult validate = passwordValidator.validate(passwordData);

校验结果会返回一个 RuleResult 对象。

3.3. RuleResult

RuleResult 包含了校验过程中的详细信息。

我们可以通过它判断密码是否合法:

Assert.assertEquals(false, validate.isValid());

✅ 同时,我们还可以获取具体的错误信息,包括错误码和参数:

RuleResultDetail ruleResultDetail = validate.getDetails().get(0);
Assert.assertEquals("TOO_SHORT", ruleResultDetail.getErrorCode());
Assert.assertEquals(5, ruleResultDetail.getParameters().get("minimumLength"));
Assert.assertEquals(5, ruleResultDetail.getParameters().get("maximumLength"));

此外,还可以通过 RuleResultMetadata 获取校验过程中的元数据:

Integer lengthCount = validate
  .getMetadata()
  .getCounts()
  .get(RuleResultMetadata.CountCategory.Length);
Assert.assertEquals(Integer.valueOf(4), lengthCount);

4. 密码生成

除了校验,Passay 还支持密码生成。✅ 我们可以为生成器指定规则,它会据此生成符合要求的密码。

生成密码需要使用 PasswordGenerator 对象,调用 generatePassword() 方法并传入一组 CharacterRule

CharacterRule digits = new CharacterRule(EnglishCharacterData.Digit);

PasswordGenerator passwordGenerator = new PasswordGenerator();
String password = passwordGenerator.generatePassword(10, digits);

Assert.assertTrue(password.length() == 10);
Assert.assertTrue(containsOnlyCharactersFromSet(password, "0123456789"));

✅ 要创建 CharacterRule,我们需要一个 CharacterData 对象。Passay 提供了 EnglishCharacterData 枚举,包含以下字符集:

  • 数字
  • 小写字母
  • 大写字母
  • 大小写混合
  • 特殊字符

当然,我们也可以自定义字符集,只需实现 CharacterData 接口即可:

CharacterRule specialCharacterRule = new CharacterRule(new CharacterData() {
    @Override
    public String getErrorCode() {
        return "SAMPLE_ERROR_CODE";
    }

    @Override
    public String getCharacters() {
        return "ABCxyz123!@#";
    }
});

PasswordGenerator passwordGenerator = new PasswordGenerator();
String password = passwordGenerator.generatePassword(10, specialCharacterRule);

Assert.assertTrue(containsOnlyCharactersFromSet(password, "ABCxyz123!@#"));

5. 正向匹配规则

我们已经了解了如何生成和校验密码。要完成这些功能,需要定义一组规则。✅ Passay 中的规则分为两类:正向匹配规则 和 负向匹配规则。

正向匹配规则接受满足特定字符、正则或长度要求的密码。

Passay 提供了以下 6 种正向匹配规则:

  • AllowedCharacterRule:密码必须包含指定字符
  • AllowedRegexRule:密码必须匹配指定正则表达式
  • CharacterRule:密码必须包含某种字符集,并满足最小数量要求
  • LengthRule:密码长度必须满足最小值
  • CharacterCharacteristicsRule:密码必须满足 N 个定义的字符规则
  • LengthComplexityRule:根据密码长度应用不同的规则

5.1. 简单正向匹配规则

以下是一些配置简单的正向规则示例:

PasswordValidator passwordValidator = new PasswordValidator(
  new AllowedCharacterRule(new char[] { 'a', 'b', 'c' }), 
  new CharacterRule(EnglishCharacterData.LowerCase, 5), 
  new LengthRule(8, 10)
);

RuleResult validate = passwordValidator.validate(new PasswordData("12abc"));

assertFalse(validate.isValid());
assertEquals(
  "ALLOWED_CHAR:{illegalCharacter=1, matchBehavior=contains}", 
  getDetail(validate, 0));
assertEquals(
  "ALLOWED_CHAR:{illegalCharacter=2, matchBehavior=contains}", 
  getDetail(validate, 1));
assertEquals(
  "TOO_SHORT:{minimumLength=8, maximumLength=10}", 
  getDetail(validate, 4));

✅ 每个规则都会返回清晰的错误信息,方便定位问题。

5.2. CharacterCharacteristicsRule

CharacterCharacteristicsRule 比前面的规则更复杂。✅ 它需要我们提供一组 CharacterRule,并指定密码需要满足其中的 N 个:

CharacterCharacteristicsRule characterCharacteristicsRule = new CharacterCharacteristicsRule(
  3, 
  new CharacterRule(EnglishCharacterData.LowerCase, 5), 
  new CharacterRule(EnglishCharacterData.UpperCase, 5), 
  new CharacterRule(EnglishCharacterData.Digit),
  new CharacterRule(EnglishCharacterData.Special)
);

5.3. LengthComplexityRule

LengthComplexityRule 允许我们根据密码长度应用不同的规则:

LengthComplexityRule lengthComplexityRule = new LengthComplexityRule();
lengthComplexityRule.addRules("[1,5]", new CharacterRule(EnglishCharacterData.LowerCase, 5));
lengthComplexityRule.addRules("[6,10]", 
  new AllowedCharacterRule(new char[] { 'a', 'b', 'c', 'd' }));

✅ 这样可以灵活应对不同长度的密码需求。

6. 负向匹配规则

与正向规则相反,负向匹配规则用于拒绝包含非法字符、正则、序列等的密码。

Passay 提供了以下负向匹配规则:

  • IllegalCharacterRule:禁止包含某些字符
  • IllegalRegexRule:禁止匹配某些正则
  • IllegalSequenceRule:禁止包含非法字符序列
  • NumberRangeRule:禁止包含指定范围内的数字
  • WhitespaceRule:禁止包含空格
  • DictionaryRule:禁止等于字典中的单词
  • DictionarySubstringRule:禁止包含字典中的单词
  • HistoryRule:禁止使用历史密码
  • DigestHistoryRule:禁止使用历史摘要密码
  • SourceRule:禁止使用来源密码
  • DigestSourceRule:禁止使用来源摘要密码
  • UsernameRule:禁止包含用户名
  • RepeatCharacterRegexRule:禁止包含重复字符

6.1. 简单负向匹配规则

以下是一些简单负向规则的使用示例:

PasswordValidator passwordValidator = new PasswordValidator(
  new IllegalCharacterRule(new char[] { 'a' }), 
  new NumberRangeRule(1, 10), 
  new WhitespaceRule()
);

RuleResult validate = passwordValidator.validate(new PasswordData("abcd22 "));

assertFalse(validate.isValid());
assertEquals(
  "ILLEGAL_CHAR:{illegalCharacter=a, matchBehavior=contains}", 
  getDetail(validate, 0));
assertEquals(
  "ILLEGAL_NUMBER_RANGE:{number=2, matchBehavior=contains}", 
  getDetail(validate, 4));
assertEquals(
  "ILLEGAL_WHITESPACE:{whitespaceCharacter= , matchBehavior=contains}", 
  getDetail(validate, 5));

✅ 每个规则都会返回明确的错误信息。

6.2. 字典规则

如果要禁止使用常见密码,可以使用 DictionaryRuleDictionarySubstringRule

WordListDictionary wordListDictionary = new WordListDictionary(
  new ArrayWordList(new String[] { "bar", "foobar" }));

DictionaryRule dictionaryRule = new DictionaryRule(wordListDictionary);
DictionarySubstringRule dictionarySubstringRule = new DictionarySubstringRule(wordListDictionary);

✅ 我们可以加载文件或数据库中的黑名单词库,防止用户使用弱密码。

6.3. 历史和来源规则

Passay 还提供了 HistoryRuleSourceRule,用于校验密码是否与历史密码或来源密码重复:

SourceRule sourceRule = new SourceRule();
HistoryRule historyRule = new HistoryRule();

PasswordData passwordData = new PasswordData("123");
passwordData.setPasswordReferences(
  new PasswordData.SourceReference("source", "password"), 
  new PasswordData.HistoricalReference("12345")
);

PasswordValidator passwordValidator = new PasswordValidator(
  historyRule, sourceRule);

✅ 可有效避免密码复用问题。

6.4. 摘要规则

Passay 还支持对摘要密码进行校验,如 DigestHistoryRuleDigestSourceRule。需要配合 EncodingHashBean 使用:

List<PasswordData.Reference> historicalReferences = Arrays.asList(
  new PasswordData.HistoricalReference(
    "SHA256",
    "2e4551de804e27aacf20f9df5be3e8cd384ed64488b21ab079fb58e8c90068ab"
));

EncodingHashBean encodingHashBean = new EncodingHashBean(
  new CodecSpec("Base64"), 
  new DigestSpec("SHA256"), 
  1, 
  false
);

✅ 然后就可以校验摘要密码是否重复:

PasswordData passwordData = new PasswordData("example!");
passwordData.setPasswordReferences(historicalReferences);

PasswordValidator passwordValidator = new PasswordValidator(new DigestHistoryRule(encodingHashBean));

RuleResult validate = passwordValidator.validate(passwordData);

Assert.assertTrue(validate.isValid());

更多关于 EncodingHashBean 的信息可以查看 Cryptacular

6.5. 重复字符规则

使用 RepeatCharacterRegexRule 可以检测密码中是否包含连续重复的 ASCII 字符:

PasswordValidator passwordValidator = new PasswordValidator(new RepeatCharacterRegexRule(3));

RuleResult validate = passwordValidator.validate(new PasswordData("aaabbb"));

assertFalse(validate.isValid());
assertEquals("ILLEGAL_MATCH:{match=aaa, pattern=([^\\x00-\\x1F])\\1{2}}", getDetail(validate, 0));

6.6. 用户名规则

UsernameRule 可以防止密码中包含用户名:

PasswordValidator passwordValidator = new PasswordValidator(new UsernameRule());

PasswordData passwordData = new PasswordData("testuser1234");
passwordData.setUsername("testuser");

RuleResult validate = passwordValidator.validate(passwordData);

assertFalse(validate.isValid());
assertEquals("ILLEGAL_USERNAME:{username=testuser, matchBehavior=contains}", getDetail(validate, 0));

7. 自定义错误信息

Passay 支持自定义校验错误信息。✅ 我们可以将错误码与自定义消息绑定,例如:

TOO_LONG=密码不能超过 %2$s 个字符。
TOO_SHORT=密码不能少于 %2$s 个字符。

然后加载配置文件并传入 PasswordValidator

URL resource = this.getClass().getClassLoader().getResource("messages.properties");
Properties props = new Properties();
props.load(new FileInputStream(resource.getPath()));

MessageResolver resolver = new PropertiesMessageResolver(props);

✅ 使用示例:

PasswordValidator validator = new PasswordValidator(
  resolver, 
  new LengthRule(8, 16), 
  new WhitespaceRule()
);

RuleResult tooShort = validator.validate(new PasswordData("XXXX"));
RuleResult tooLong = validator.validate(new PasswordData("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"));

Assert.assertEquals(
  "密码不能少于 16 个字符。", 
  validator.getMessages(tooShort).get(0));
Assert.assertEquals(
  "密码不能超过 16 个字符。", 
  validator.getMessages(tooLong).get(0));

8. 总结

本指南介绍了 Passay 库的基本用法,涵盖了密码生成、校验以及丰富的规则体系。✅ Passay 能满足大多数密码策略需求,但记住,它只是一个工具,真正的安全策略还需要我们结合业务场景合理设计。


原始标题:Guide to Passay | Baeldung