diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java b/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java index 3af7f4b..ccac19b 100644 --- a/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java +++ b/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java @@ -51,11 +51,11 @@ public class PasswordValidatorService { return AjaxResult.error("密码包含常见的弱密码片段!"); } - if (containsConsecutiveCharacters(newPassword, config.getMaxConsecutiveChars())) { - return AjaxResult.error("密码不能包含超过" + config.getMaxConsecutiveChars() + "位连续字符!"); + if (containsConsecutiveCharacters(newPassword.toLowerCase(), config.getMaxConsecutiveChars())) { + return AjaxResult.error("密码不能包含超过" + config.getMaxConsecutiveChars() + "个连续相同字符、连续递增/递减的数字或字母(不区分大小写)"); } - if (newPassword.contains(username)) { + if (newPassword.toLowerCase().contains(username.toLowerCase())) { return AjaxResult.error("密码不能包含账号!"); } @@ -120,7 +120,7 @@ public class PasswordValidatorService { */ private boolean containsWeakPassword(String password) { for (String weakPwd : config.getWeakPasswords()) { - if (password.contains(weakPwd)) { + if (password.toLowerCase().contains(weakPwd)) { return true; } } @@ -128,24 +128,83 @@ public class PasswordValidatorService { } /** - * 检查密码中是否包含超过 n 个连续相同字符 + * 检查密码中是否包含超过 n 个连续相同字符、连续递增/递减的数字或字母(不区分大小写) */ private boolean containsConsecutiveCharacters(String password, int n) { + // 检查连续相同字符 + n = n + 1; for (int i = 0; i <= password.length() - n; i++) { - boolean consecutive = true; + boolean consecutiveSameChar = true; for (int j = 1; j < n; j++) { if (password.charAt(i + j) != password.charAt(i)) { - consecutive = false; + consecutiveSameChar = false; break; } } - if (consecutive) { + if (consecutiveSameChar) { + return true; // 包含超过 n 个连续相同字符 + } + } + + // 检查连续递增或递减的数字 + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutiveIncreasing = true; + boolean consecutiveDecreasing = true; + for (int j = 1; j < n; j++) { + char currentChar = password.charAt(i); + char nextChar = password.charAt(i + j); + + // 检查数字递增或递减 + if (Character.isDigit(currentChar) && Character.isDigit(nextChar)) { + if (nextChar != currentChar + j) { + consecutiveIncreasing = false; + } + if (nextChar != currentChar - j) { + consecutiveDecreasing = false; + } + } else { + consecutiveIncreasing = false; + consecutiveDecreasing = false; + break; + } + } + if (consecutiveIncreasing || consecutiveDecreasing) { + return true; // 包含超过 n 个递增或递减的连续数字 + } + } + + // 检查连续递增或递减的字母(不区分大小写) + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutiveIncreasing = true; + boolean consecutiveDecreasing = true; + for (int j = 1; j < n; j++) { + char currentChar = Character.toLowerCase(password.charAt(i)); // 转为小写 + char nextChar = Character.toLowerCase(password.charAt(i + j)); // 转为小写 + + // 检查字母递增或递减 + if (Character.isLetter(currentChar) && Character.isLetter(nextChar)) { + if (nextChar != currentChar + j) { + consecutiveIncreasing = false; + } + if (nextChar != currentChar - j) { + consecutiveDecreasing = false; + } + } else { + consecutiveIncreasing = false; + consecutiveDecreasing = false; + break; + } + } + if (consecutiveIncreasing || consecutiveDecreasing) { + // 包含超过 n 个递增或递减的连续字母 return true; } } + // 不包含连续相同字符、数字或字母序列 return false; } + /** * 验证登录参数 */ diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java index 0e29db8..fcdf272 100644 --- a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java @@ -14,11 +14,10 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; -/** - * 密码校验器类,用于根据配置对用户输入的密码进行验证。 - */ @Service public class PasswordValidatorServiceImpl implements PasswordValidatorService { @@ -31,195 +30,233 @@ public class PasswordValidatorServiceImpl implements PasswordValidatorService { @Resource private PasswordValidatorMapper passwordValidatorMapper; - @Override public AjaxResult validatePassword(Long userId, String username, String oldPassword, String newPassword) { - // 1. 检查密码长度是否符合配置 - if (newPassword.length() < config.getMinLength() || newPassword.length() > config.getMaxLength()) { + // 1. 检查密码长度 + if (!isPasswordLengthValid(newPassword)) { return AjaxResult.error("密码长度应为" + config.getMinLength() + "至" + config.getMaxLength() + "位!"); } - // 2. 检查密码是否包含大写字母、小写字母、数字和特殊字符 - boolean hasUpperCase = false, hasLowerCase = false, hasDigit = false, hasSpecialChar = false; - for (char c : newPassword.toCharArray()) { - if (Character.isUpperCase(c)) { - hasUpperCase = true; - } - if (Character.isLowerCase(c)) { - hasLowerCase = true; - } - if (Character.isDigit(c)) { - hasDigit = true; - } - if ("!@#$%^&*()-_=+[{]};:'\",<.>/?".indexOf(c) >= 0) { - hasSpecialChar = true; - } + + // 2. 检查密码字符类型 + if (!isPasswordCharacterValid(newPassword)) { + return AjaxResult.error(getCharacterRequirementErrorMessage()); } - // 根据配置检查大写字母、小写字母、数字和特殊字符的要求 - if (config.isRequireUpperCase() && !hasUpperCase) { - return AjaxResult.error("密码必须包含大写字母!"); - + // 3. 检查常见弱密码 + if (containsWeakPassword(newPassword.toLowerCase())) { + return AjaxResult.error("密码包含常见的弱密码片段!"); } - if (config.isRequireLowerCase() && !hasLowerCase) { - return AjaxResult.error("密码必须包含小写字母!"); - } - - if (config.isRequireDigit() && !hasDigit) { - return AjaxResult.error("密码必须包含数字!"); - } - - if (config.isRequireSpecialChar() && !hasSpecialChar) { - return AjaxResult.error("密码必须包含特殊字符!"); - } - - // 3. 检查是否包含常见弱密码 - for (String weakPwd : config.getWeakPasswords()) { - if (newPassword.contains(weakPwd)) { - return AjaxResult.error("密码包含常见的弱密码片段: " + weakPwd); - } - } - - // 4. 检查是否包含超过规定数量的连续字符 - if (config.isRestrictConsecutiveChars() && containsConsecutiveCharacters(newPassword, config.getMaxConsecutiveChars())) { + // 4. 检查连续字符 + if (config.isRestrictConsecutiveChars() && containsConsecutiveCharacters(newPassword.toLowerCase(), config.getMaxConsecutiveChars())) { return AjaxResult.error("密码不能包含超过" + config.getMaxConsecutiveChars() + "位连续字符!"); } - // 5. 检查密码中是否包含用户名 - if (config.isExcludeUsernameInPassword() && newPassword.contains(username)) { + // 5. 检查用户名 + if (config.isExcludeUsernameInPassword() && newPassword.toLowerCase().contains(username.toLowerCase())) { return AjaxResult.error("密码不能包含账号!"); } - // 6. 检查新密码是否与旧密码相同 + // 6. 检查新旧密码是否相同 if (SecurityUtils.matchesPassword(newPassword, oldPassword)) { return AjaxResult.error("新密码不能与原密码相同!"); } - // 7. 检查新密码是否与历史密码相同 - List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); - // 如果没有找到密码历史记录,返回提示 - if (userPasswordHistories.isEmpty()) { - return AjaxResult.success("没有找到密码历史记录。"); - } - // 只取最近的五次修改记录 - // 使用 Math.min 以确保不会超过实际记录数 - int limit = Math.min(userPasswordHistories.size(), config.getPasswordHistoryLimit()); - for (int i = 0; i < limit; i++) { - UserPasswordHistory history = userPasswordHistories.get(i); - // 比较新密码与历史密码 - if (SecurityUtils.matchesPassword(newPassword, history.getNewPassword())) { - return AjaxResult.error("新密码不能与最近的" + config.getPasswordHistoryLimit() + "个旧密码相同!"); - } + // 7. 检查密码历史 + if (isPasswordInHistory(userId, newPassword)) { + return AjaxResult.error("新密码不能与最近的" + config.getPasswordHistoryLimit() + "个旧密码相同!"); } return AjaxResult.success(); } + private boolean isPasswordLengthValid(String password) { + return password.length() >= config.getMinLength() && password.length() <= config.getMaxLength(); + } + + private boolean isPasswordCharacterValid(String password) { + boolean hasUpperCase = false, hasLowerCase = false, hasDigit = false, hasSpecialChar = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpperCase = true; + if (Character.isLowerCase(c)) hasLowerCase = true; + if (Character.isDigit(c)) hasDigit = true; + if ("!@#$%^&*()-_=+[{]};:'\",<.>/?".indexOf(c) >= 0) hasSpecialChar = true; + } + + if (config.isRequireUpperCase() && !hasUpperCase) return false; + if (config.isRequireLowerCase() && !hasLowerCase) return false; + if (config.isRequireDigit() && !hasDigit) return false; + return !(config.isRequireSpecialChar() && !hasSpecialChar); + } + + private boolean containsWeakPassword(String password) { + Set weakPasswords = new HashSet<>(config.getWeakPasswords()); + for (String weakPwd : weakPasswords) { + if (password.contains(weakPwd)) { + return true; + } + } + return false; + } + /** - * 检查用户是否是首次登录 - * - * @return 如果密码符合策略要求,则返回 true,否则返回 false + * 根据配置返回密码不符合要求时的错误提示信息 */ + private String getCharacterRequirementErrorMessage() { + if (config.isRequireUpperCase()) { + return "密码必须包含大写字母!"; + } + if (config.isRequireLowerCase()) { + return "密码必须包含小写字母!"; + } + if (config.isRequireDigit()) { + return "密码必须包含数字!"; + } + if (config.isRequireSpecialChar()) { + return "密码必须包含特殊字符!"; + } + return "密码不符合字符要求!"; + } + + private boolean isPasswordInHistory(Long userId, String newPassword) { + List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); + if (userPasswordHistories.isEmpty()) return false; + + // 只取最近的五次修改记录 + int limit = Math.min(userPasswordHistories.size(), config.getPasswordHistoryLimit()); + for (int i = 0; i < limit; i++) { + UserPasswordHistory history = userPasswordHistories.get(i); + if (SecurityUtils.matchesPassword(newPassword, history.getNewPassword())) { + return true; + } + } + return false; + } + @Override public boolean checkFirstLogin() { - // 如果未开启首次登录强制修改密码功能,直接返回错误 if (!config.isForcePasswordChangeOnFirstLogin()) { return false; } LoginUser loginUser = SecurityUtils.getLoginUser(); SysUser sysUser = loginUser.getSysUser(); - // 获取初始密码配置 String initialPassword = configService.selectConfigByKey("sys.user.initPassword"); - // 检查当前用户密码是否是初始密码 - // 根据检查结果返回相应的响应 return SecurityUtils.matchesPassword(initialPassword, sysUser.getPassword()); } - /** - * 检查用户密码是否已过期,提示用户定期修改密码。 - * - * @return 如果需要修改密码,返回 AjaxResult 提示用户修改,否则返回成功信息 - */ @Override public boolean checkPasswordExpiry() { Long userId = SecurityUtils.getUserId(); List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); - // 如果没有找到密码历史记录,返回错误 if (userPasswordHistories.isEmpty()) { return false; } - // 获取最近的密码修改记录 UserPasswordHistory userPasswordHistory = userPasswordHistories.get(0); - // 获取当前日期 Date currentDate = DateUtils.getNowDate(); - // 获取最近密码修改日期 Date changeDate = DateUtils.toDate(userPasswordHistory.getChangeTimestamp()); - // 计算最近密码修改到当前日期的天数 long daysSinceChange = DateUtils.daysBetween(changeDate, currentDate); - // 检查是否达到定期修改密码的天数要求 return daysSinceChange > config.getRegularlyChangePassword(); } - /** - * 检查新密码是否在最近五次修改的密码中 - * - * @param userId 用户ID - * @param newPassword 新密码 - * @return 如果新密码在最近的五次修改记录中,返回错误提示,否则返回成功提示 - */ @Override public AjaxResult isPasswordInRecentHistory(Long userId, String newPassword) { List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); - - // 如果没有找到密码历史记录,返回提示 if (userPasswordHistories.isEmpty()) { return AjaxResult.success("没有找到密码历史记录。"); } - // 只取最近的五次修改记录 - // 使用 Math.min 以确保不会超过实际记录数 int limit = Math.min(userPasswordHistories.size(), config.getPasswordHistoryLimit()); for (int i = 0; i < limit; i++) { UserPasswordHistory history = userPasswordHistories.get(i); - // 比较新密码与历史密码 if (SecurityUtils.matchesPassword(newPassword, history.getNewPassword())) { - return AjaxResult.error("新密码不能与最近的密码相同。"); // 新密码在最近五次记录中 + return AjaxResult.error("新密码不能与最近的密码相同。"); } } - return AjaxResult.success("新密码有效。"); // 新密码不在记录中 + return AjaxResult.success("新密码有效。"); } - /** - * @param userPasswordHistory - * @return - */ @Override public int addPasswordExpiry(UserPasswordHistory userPasswordHistory) { return passwordValidatorMapper.addPasswordExpiry(userPasswordHistory); } /** - * 帮助方法,用于检测密码是否包含超过 n 个连续相同字符。 - * - * @param password 需要检查的密码 - * @param n 允许的最大连续相同字符数量 - * @return 如果包含连续字符,则返回 true,否则返回 false + * 检查密码中是否包含超过 n 个连续相同字符、连续递增/递减的数字或字母(不区分大小写) */ private boolean containsConsecutiveCharacters(String password, int n) { + // 检查连续相同字符 + n = n + 1; for (int i = 0; i <= password.length() - n; i++) { - boolean consecutive = true; + boolean consecutiveSameChar = true; for (int j = 1; j < n; j++) { if (password.charAt(i + j) != password.charAt(i)) { - consecutive = false; + consecutiveSameChar = false; break; } } - if (consecutive) { + if (consecutiveSameChar) { + return true; // 包含超过 n 个连续相同字符 + } + } + + // 检查连续递增或递减的数字 + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutiveIncreasing = true; + boolean consecutiveDecreasing = true; + for (int j = 1; j < n; j++) { + char currentChar = password.charAt(i); + char nextChar = password.charAt(i + j); + + // 检查数字递增或递减 + if (Character.isDigit(currentChar) && Character.isDigit(nextChar)) { + if (nextChar != currentChar + j) { + consecutiveIncreasing = false; + } + if (nextChar != currentChar - j) { + consecutiveDecreasing = false; + } + } else { + consecutiveIncreasing = false; + consecutiveDecreasing = false; + break; + } + } + if (consecutiveIncreasing || consecutiveDecreasing) { + return true; // 包含超过 n 个递增或递减的连续数字 + } + } + + // 检查连续递增或递减的字母(不区分大小写) + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutiveIncreasing = true; + boolean consecutiveDecreasing = true; + for (int j = 1; j < n; j++) { + char currentChar = Character.toLowerCase(password.charAt(i)); // 转为小写 + char nextChar = Character.toLowerCase(password.charAt(i + j)); // 转为小写 + + // 检查字母递增或递减 + if (Character.isLetter(currentChar) && Character.isLetter(nextChar)) { + if (nextChar != currentChar + j) { + consecutiveIncreasing = false; + } + if (nextChar != currentChar - j) { + consecutiveDecreasing = false; + } + } else { + consecutiveIncreasing = false; + consecutiveDecreasing = false; + break; + } + } + if (consecutiveIncreasing || consecutiveDecreasing) { + // 包含超过 n 个递增或递减的连续字母 return true; } } + // 不包含连续相同字符、数字或字母序列 return false; } }