基于 Passay 的密码生成与校验方案
1. 背景与目标
为规范密码的生成与使用,特制定本密码生成与校验方案。
1.1 密码管理核心要求
| 要求项 | 具体规则 |
|---|---|
| 密码长度 | 最小12位,最大20位 |
| 字符种类 | 至少包含大写字母、小写字母、数字、特殊字符中的3 种(本实现采用4 种全包含) |
| 禁止连续序列 | 禁止 3 位及以上的连续字母(如 abc)、连续数字(如 123)、键盘序列(如 qwert) |
| 禁止重复字符 | 禁止 3 个及以上相同字符连续出现(如 aaa、111) |
| 禁止弱口令 | 内置弱口令字典(admin、password、123456 等 30+ 条) |
| 禁止个人信息 | 禁止包含用户名、手机号、身份证号、邮政编码、年月日期 |
| 禁止空格 | 密码中不允许包含空白字符 |
2. 技术选型
2.1 为什么选择 Passay?
Passay 是一个 Java 密码策略库,提供密码生成和合规校验功能。它能根据长度、字符类型、连续序列、重复字符、字典等规则自动生成随机密码,并对用户设置的密码进行多维度验证。
结论:Passay 是 Java 生态中专门用于规则驱动型密码生成与校验的成熟方案。
2.2 Maven 依赖
xml
<dependency> <groupId>org.passay</groupId> <artifactId>passay</artifactId> <version>1.6.6</version> </dependency>
3. 核心代码实现
3.1 特殊字符集定义(SpecialCharData.java)
java
package com.example.password.util; import org.passay.CharacterData; public enum SpecialCharData implements CharacterData { RANGE("INSUFFICIENT_SPECIAL", "!@#$%&*+-_=.?~^()[]{}:;'<>,/"); private final String errorCode; private final String characters; SpecialCharData(String code, String charString) { this.errorCode = code; this.characters = charString; } @Override public String getErrorCode() { return this.errorCode; } @Override public String getCharacters() { return this.characters; } }3.2 密码规则配置(PassayRuleConfig.java)
java
package com.example.password.config; import com.example.password.util.SpecialCharData; import org.passay.*; import org.passay.dictionary.ArrayWordList; import org.passay.dictionary.WordListDictionary; import org.passay.dictionary.sort.ArraysSort; import org.passay.spring.SpringMessageResolver; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @Configuration public class PassayRuleConfig { @Resource private MessageSource messageSource; private static final WordListDictionary WORD_LIST_DICTIONARY; private static final List<Rule> RULE_LIST; static { WORD_LIST_DICTIONARY = new WordListDictionary(new ArrayWordList(new String[]{ "520", "1314", "woaini", "iloveyou", "loveme", "admin", "root", "administrator", "test", "guest", "user", "default", "admin123", "admin@123", "qq123", "abc123", "passwd", "password", "123456", "12345678", "123456789", "1234567890", "qwerty", "qwert", "asdf", "asdfgh", "zxcvbn", "letmein", "welcome", "monkey", "dragon" }, false, new ArraysSort())); RULE_LIST = Arrays.asList( new LengthRule(12, 20), new CharacterRule(EnglishCharacterData.UpperCase, 1), new CharacterRule(EnglishCharacterData.LowerCase, 1), new CharacterRule(EnglishCharacterData.Digit, 1), new CharacterRule(SpecialCharData.RANGE, 1), new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 3, false), new IllegalSequenceRule(EnglishSequenceData.Numerical, 3, false), new IllegalSequenceRule(EnglishSequenceData.USQwerty, 3, false), new RepeatCharactersRule(3), new DictionaryRule(WORD_LIST_DICTIONARY), new DictionarySubstringRule(WORD_LIST_DICTIONARY), new UsernameRule(), new IllegalRegexRuleExtended("CUSTOMIZATION_DATE_MATCH", "(?:19|20)\\d{2}(?:0[1-9]|1[0-2])", true), new IllegalRegexRuleExtended("CUSTOMIZATION_POSTCODE_MATCH", "\\b[1-9]\\d{5}\\b", true), new IllegalRegexRuleExtended("CUSTOMIZATION_IDENTITYCARD_MATCH", "\\b(?:(?:\\d{8}(?:0[1-9]|1[0-2])(?:[0-2]\\d|3[01])\\d{3})|(?:\\d{6}(?:18|19|20)\\d{2}(?:0[1-9]|1[0-2])(?:[0-2]\\d|3[01])\\d{3}[\\dXx]))\\b", true), new IllegalRegexRuleExtended("CUSTOMIZATION_PHONE_MATCH", "\\b(?:(?:\\+|00)86)?1(?:3\\d|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8\\d|9[189])\\d{8}\\b", true), new WhitespaceRule()); } @Bean public PasswordValidator passwordValidator() { SpringMessageResolver messageResolver = new SpringMessageResolver(messageSource, Locale.getDefault()); return new PasswordValidator(messageResolver, RULE_LIST); } private static List<Rule> validRuleList() { return RULE_LIST; } public static class IllegalRegexRuleExtended implements Rule { public final String errorCode; protected final Pattern pattern; protected final boolean reportAllFailures; public IllegalRegexRuleExtended(String errorCode, String regex, boolean reportAll) { this.errorCode = errorCode; this.pattern = Pattern.compile(regex); this.reportAllFailures = reportAll; } @Override public RuleResult validate(PasswordData passwordData) { RuleResult result = new RuleResult(); Matcher m = this.pattern.matcher(passwordData.getPassword()); Set<String> matches = new HashSet<>(); while (m.find()) { String match = m.group(); if (!matches.contains(match)) { result.addError(this.errorCode, this.createRuleResultDetailParameters(match)); if (!this.reportAllFailures) { break; } matches.add(match); } } return result; } protected Map<String, Object> createRuleResultDetailParameters(String match) { Map<String, Object> m = new LinkedHashMap<>(2); m.put("match", match); m.put("pattern", this.pattern); return m; } @Override public String toString() { return String.format("%s@%h::pattern=%s", this.getClass().getName(), this.hashCode(), this.pattern); } } }3.3 密码工具类(PassWordUtil.java)
java
package com.example.password.util; import org.passay.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Arrays; import java.util.List; @Component public class PassWordUtil { private static final Logger log = LoggerFactory.getLogger(PassWordUtil.class); @Resource private PasswordValidator passwordValidator; @Value("${password.config.max-try-count}") private Integer maxTryCount; @Value("${password.config.generator-password-length}") private Integer generatorPasswordLength; public ValidUserPasswordBean validUserPassword(String userNo, String password) { if (password == null || password.isEmpty()) { return new ValidUserPasswordBean(false, "密码不能为空"); } if (userNo == null || userNo.isEmpty()) { return new ValidUserPasswordBean(false, "用户编号不能为空"); } PasswordData passwordData = new PasswordData(); passwordData.setUsername(userNo); passwordData.setPassword(password); RuleResult validate = passwordValidator.validate(passwordData); boolean valid = validate.isValid(); if (valid) { return new ValidUserPasswordBean(true, ""); } List<String> validatorMessages = passwordValidator.getMessages(validate); return new ValidUserPasswordBean(false, String.join(";", validatorMessages)); } public static class ValidUserPasswordBean { private boolean success; private String msg; public ValidUserPasswordBean(boolean success, String msg) { this.success = success; this.msg = msg; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } } private static List<CharacterRule> createPasswordRuleList() { return Arrays.asList( new CharacterRule(EnglishCharacterData.UpperCase, 1), new CharacterRule(EnglishCharacterData.LowerCase, 1), new CharacterRule(EnglishCharacterData.Digit, 1), new CharacterRule(SpecialCharData.RANGE, 1)); } public String createPassword(String userNo) { PasswordGenerator generator = new PasswordGenerator(); String password = generator.generatePassword(generatorPasswordLength, createPasswordRuleList()); int tryCount = 0; while (tryCount <= maxTryCount) { ValidUserPasswordBean validUserPasswordBean = this.validUserPassword(userNo, password); if (validUserPasswordBean.isSuccess()) { return password; } password = generator.generatePassword(generatorPasswordLength, createPasswordRuleList()); tryCount++; } log.error("生成密码失败:用户[{}],超过最大重试次数({}),无法生成符合规则的密码", userNo, maxTryCount); throw new RuntimeException("生成密码失败:超过最大重试次数(" + maxTryCount + "),无法生成符合规则的密码"); } }3.4 国际化消息配置
3.4.1 messages.properties(默认/英文)
properties
# Passay properties HISTORY_VIOLATION=Password matches one of {0} previous passwords. ILLEGAL_WORD=Password contains the dictionary word '{0}'. ILLEGAL_WORD_REVERSED=Password contains the reversed dictionary word '{0}'. ILLEGAL_DIGEST_WORD=Password contains a dictionary word. ILLEGAL_DIGEST_WORD_REVERSED=Password contains a reversed dictionary word. ILLEGAL_MATCH=Password matches the illegal pattern '{0}'. ALLOWED_MATCH=Password must match pattern '{0}'. ILLEGAL_CHAR=Password {1} the illegal character '{0}'. ALLOWED_CHAR=Password {1} the illegal character '{0}'. ILLEGAL_QWERTY_SEQUENCE=Password contains the illegal QWERTY sequence '{0}'. ILLEGAL_ALPHABETICAL_SEQUENCE=Password contains the illegal alphabetical sequence '{0}'. ILLEGAL_NUMERICAL_SEQUENCE=Password contains the illegal numerical sequence '{0}'. ILLEGAL_USERNAME=Password {1} the user id '{0}'. ILLEGAL_USERNAME_REVERSED=Password {1} the user id '{0}' in reverse. ILLEGAL_WHITESPACE=Password {1} a whitespace character. ILLEGAL_NUMBER_RANGE=Password {1} the number '{0}'. ILLEGAL_REPEATED_CHARS=Password contains {2} sequences of {0} or more repeated characters, but only {1} allowed: {3}. INSUFFICIENT_UPPERCASE=Password must contain {0} or more uppercase characters. INSUFFICIENT_LOWERCASE=Password must contain {0} or more lowercase characters. INSUFFICIENT_ALPHABETICAL=Password must contain {0} or more alphabetical characters. INSUFFICIENT_DIGIT=Password must contain {0} or more digit characters. INSUFFICIENT_SPECIAL=Password must contain {0} or more special characters. INSUFFICIENT_CHARACTERISTICS=Password matches {0} of {2} character rules, but {1} are required. INSUFFICIENT_COMPLEXITY=Password meets {1} complexity rules, but {2} are required. INSUFFICIENT_COMPLEXITY_RULES=No rules have been configured for a password of length {0}. SOURCE_VIOLATION=Password cannot be the same as your {0} password. TOO_LONG=Password must be no more than {1} characters in length. TOO_SHORT=Password must be {0} or more characters in length. TOO_MANY_OCCURRENCES=Password contains {1} occurrences of the character '{0}', but at most {2} are allowed. CUSTOMIZATION_DATE_MATCH=Password contains date format characters. CUSTOMIZATION_POSTCODE_MATCH=Password contains postal code. CUSTOMIZATION_PHONE_MATCH=Password contains phone number. CUSTOMIZATION_IDENTITYCARD_MATCH=Password contains identity card number.3.4.2 messages_en_US.properties(英文-美国)
properties
HISTORY_VIOLATION=Password matches one of {0} previous passwords. ILLEGAL_WORD=Password contains the dictionary word '{0}'. ILLEGAL_WORD_REVERSED=Password contains the reversed dictionary word '{0}'. ILLEGAL_DIGEST_WORD=Password contains a dictionary word. ILLEGAL_DIGEST_WORD_REVERSED=Password contains a reversed dictionary word. ILLEGAL_MATCH=Password matches the illegal pattern '{0}'. ALLOWED_MATCH=Password must match pattern '{0}'. ILLEGAL_CHAR=Password {1} the illegal character '{0}'. ALLOWED_CHAR=Password {1} the illegal character '{0}'. ILLEGAL_QWERTY_SEQUENCE=Password contains the illegal QWERTY sequence '{0}'. ILLEGAL_ALPHABETICAL_SEQUENCE=Password contains the illegal alphabetical sequence '{0}'. ILLEGAL_NUMERICAL_SEQUENCE=Password contains the illegal numerical sequence '{0}'. ILLEGAL_USERNAME=Password {1} the user id '{0}'. ILLEGAL_USERNAME_REVERSED=Password {1} the user id '{0}' in reverse. ILLEGAL_WHITESPACE=Password {1} a whitespace character. ILLEGAL_NUMBER_RANGE=Password {1} the number '{0}'. ILLEGAL_REPEATED_CHARS=Password contains {2} sequences of {0} or more repeated characters, but only {1} allowed: {3}. INSUFFICIENT_UPPERCASE=Password must contain {0} or more uppercase characters. INSUFFICIENT_LOWERCASE=Password must contain {0} or more lowercase characters. INSUFFICIENT_ALPHABETICAL=Password must contain {0} or more alphabetical characters. INSUFFICIENT_DIGIT=Password must contain {0} or more digit characters. INSUFFICIENT_SPECIAL=Password must contain {0} or more special characters. INSUFFICIENT_CHARACTERISTICS=Password matches {0} of {2} character rules, but {1} are required. INSUFFICIENT_COMPLEXITY=Password meets {1} complexity rules, but {2} are required. INSUFFICIENT_COMPLEXITY_RULES=No rules have been configured for a password of length {0}. SOURCE_VIOLATION=Password cannot be the same as your {0} password. TOO_LONG=Password must be no more than {1} characters in length. TOO_SHORT=Password must be {0} or more characters in length. TOO_MANY_OCCURRENCES=Password contains {1} occurrences of the character '{0}', but at most {2} are allowed. CUSTOMIZATION_DATE_MATCH=Password contains date format characters. CUSTOMIZATION_POSTCODE_MATCH=Password contains postal code. CUSTOMIZATION_PHONE_MATCH=Password contains phone number. CUSTOMIZATION_IDENTITYCARD_MATCH=Password contains identity card number.3.4.3 messages_zh_CN.properties(中文-中国)
properties
# Passay 属性 HISTORY_VIOLATION=密码和您最近用过的 {0} 个密码之一重复 ILLEGAL_WORD=密码包含了保留字典中的词 {0} ILLEGAL_WORD_REVERSED=密码包含了保留字典中的词 {0} ILLEGAL_DIGEST_WORD=密码包含了字典中的词 ILLEGAL_DIGEST_WORD_REVERSED=密码包含了保留字典中的词 ILLEGAL_MATCH=密码匹配了非法结构 {0} ALLOWED_MATCH=密码必须要匹配结构 {0} ILLEGAL_CHAR=密码 {1} 非法字符 {0} ALLOWED_CHAR=密码 {1} 非法字符 {0} ILLEGAL_QWERTY_SEQUENCE=密码包含连续的键盘字符 {0} ILLEGAL_ALPHABETICAL_SEQUENCE=密码包含非法的字母序列 {0} ILLEGAL_NUMERICAL_SEQUENCE=密码包含非法的数字序列 {0} ILLEGAL_USERNAME=密码包含用户名 {0} ILLEGAL_USERNAME_REVERSED=密码 {1} 倒序的用户 id {0} ILLEGAL_WHITESPACE=密码 {1} 空格 ILLEGAL_NUMBER_RANGE=密码 {1} 数字 {0}. ILLEGAL_REPEATED_CHARS=密码中包含 {2} 序列 {0} 的一个或多个重复字符, 但仅允许 {1} 个: {3} INSUFFICIENT_UPPERCASE=密码中必须包含至少 {0} 个大写字母 INSUFFICIENT_LOWERCASE=密码中必须包含至少 {0} 个小写字母 INSUFFICIENT_ALPHABETICAL=密码中必须包含至少 {0} 个字母 INSUFFICIENT_DIGIT=密码中必须包含至少 {0} 个数字 INSUFFICIENT_SPECIAL=密码中必须包含至少 {0} 个特殊字符 INSUFFICIENT_CHARACTERISTICS=密码匹配了 {0} of {2} 字符规则, 但只允许 {1} 个 INSUFFICIENT_COMPLEXITY=密码符合了 {1} 个复杂规则, 但需要符合 {2} 个 INSUFFICIENT_COMPLEXITY_RULES=对于密码长度 {0},没有配置规则 SOURCE_VIOLATION=密码不能和之前的 {0} 个历史密码相同 TOO_LONG=密码长度不能超过 {1} 个字符 TOO_SHORT=密码长度不能少于 {0} 个字符 TOO_MANY_OCCURRENCES=密码包含 {1} 个 {0}, 但是至多只允许 {2} 个 CUSTOMIZATION_DATE_MATCH=密码中包含年月字符 CUSTOMIZATION_POSTCODE_MATCH=密码中包含邮政编码 CUSTOMIZATION_PHONE_MATCH=密码中包含手机号 CUSTOMIZATION_IDENTITYCARD_MATCH=密码中包含身份证号3.5 配置文件(application.yml)
yaml
password: config: max-try-count: 10 # 生成密码最大重试次数 generator-password-length: 16 # 生成的密码长度
4. 规则与规范对照表
| 规范要求 | 对应 Rule | 说明 |
|---|---|---|
| 长度不少于12位,不超过20位 | LengthRule(12, 20) | 完全满足 |
| 包含大写、小写、数字、特殊字符 | CharacterRule(..., 1)× 4 | 四类字符各至少1个 |
| 不使用简单数字/字母组合 | IllegalSequenceRule(Alphabetical, 3) | 禁止 abc、def 等连续3位字母 |
| 不使用简单数字/字母组合 | IllegalSequenceRule(Numerical, 3) | 禁止 123、456 等连续3位数字 |
| 不使用键盘连续按键 | IllegalSequenceRule(USQwerty, 3) | 禁止 qwert、asdf 等键盘序列 |
| 不使用重复字符 | RepeatCharactersRule(3) | 禁止 aaa、111 等3位重复 |
| 不使用默认/常见弱口令 | DictionaryRule+DictionarySubstringRule | 内置30+弱口令字典 |
| 不与用户名相同/相关 | UsernameRule | 禁止包含用户名 |
| 不使用手机号 | IllegalRegexRuleExtended(手机号正则) | 匹配11位手机号 |
| 不使用身份证号 | IllegalRegexRuleExtended(身份证正则) | 匹配15/18位身份证 |
| 不使用邮政编码 | IllegalRegexRuleExtended(邮编正则) | 匹配6位邮编 |
| 不使用年月日期 | IllegalRegexRuleExtended(日期正则) | 匹配 YYYYMM 格式 |
| 不包含空格 | WhitespaceRule | 禁止空白字符 |
5. 方案总结
| 维度 | 评价 |
|---|---|
| 规则覆盖 | 完全覆盖密码管理规范的每一条要求 |
| 代码质量 | 生产可用,职责清晰,可维护性强 |
| 扩展性 | 支持自定义规则(如IllegalRegexRuleExtended) |
| 国际化 | 支持中英文错误消息 |
| 性能 | 密码生成 500 个耗时极低,重试机制保证成功率 |
| 维护状态 | Passay 持续活跃,无过时风险 |
6. 附录:生成的密码示例
以下为系统自动生成的符合规范的密码示例(16位):
| 序号 | 密码 | 包含大写 | 包含小写 | 包含数字 | 包含特殊字符 |
|---|---|---|---|---|---|
| 1 | V^PCO6U[~kBs#]KM | ✅ | ✅ | ✅ | ✅ |
| 2 | >!$sw_4-%P^mQG7e | ✅ | ✅ | ✅ | ✅ |
| 3 | 6@/Re:8hbW5)@.$} | ✅ | ✅ | ✅ | ✅ |
| 4 | <{DSp[to0-re1*{s | ✅ | ✅ | ✅ | ✅ |
| 5 | 6}n+2bS]Lb}&=1W* | ✅ | ✅ | ✅ | ✅ |
所有生成的密码均通过完整规则校验。