mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 00:57:13 +08:00 
			
		
		
		
	refactor: 完善密码策略取值范围校验
This commit is contained in:
		| @@ -16,6 +16,9 @@ | ||||
|  | ||||
| package top.continew.admin.system.enums; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.ObjUtil; | ||||
| import cn.hutool.core.util.ReUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.extra.spring.SpringUtil; | ||||
| @@ -24,9 +27,12 @@ import lombok.RequiredArgsConstructor; | ||||
| import top.continew.admin.common.constant.RegexConstants; | ||||
| import top.continew.admin.common.constant.SysConstants; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.service.OptionService; | ||||
| import top.continew.admin.system.service.UserPasswordHistoryService; | ||||
| import top.continew.starter.core.util.validate.ValidationUtils; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 密码策略枚举 | ||||
|  * | ||||
| @@ -38,43 +44,89 @@ import top.continew.starter.core.util.validate.ValidationUtils; | ||||
| @RequiredArgsConstructor | ||||
| public enum PasswordPolicyEnum { | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的次数 | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_COUNT("登录密码错误锁定账号的次数取值范围为 %d-%d", SysConstants.NO, 10, null), | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的时间(min) | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_MINUTES("登录密码错误锁定账号的时间取值范围为 %d-%d 分钟", 1, 1440, null), | ||||
|  | ||||
|     /** | ||||
|      * 密码有效期(天) | ||||
|      */ | ||||
|     PASSWORD_EXPIRATION_DAYS("密码有效期取值范围为 %d-%d 天", SysConstants.NO, 999, null), | ||||
|  | ||||
|     /** | ||||
|      * 密码到期提前提示(天) | ||||
|      */ | ||||
|     PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提前提示取值范围为 %d-%d 天", SysConstants.NO, 998, null) { | ||||
|         @Override | ||||
|         public void validateRange(int value, Map<String, String> policyMap) { | ||||
|             if (CollUtil.isEmpty(policyMap)) { | ||||
|                 super.validateRange(value, policyMap); | ||||
|                 return; | ||||
|             } | ||||
|             Integer passwordExpirationDays = ObjUtil.defaultIfNull(Convert.toInt(policyMap.get(PASSWORD_EXPIRATION_DAYS | ||||
|                 .name())), SpringUtil.getBean(OptionService.class).getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); | ||||
|             if (passwordExpirationDays > SysConstants.NO) { | ||||
|                 ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期前的提示时间应小于密码有效期"); | ||||
|                 return; | ||||
|             } | ||||
|             super.validateRange(value, policyMap); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码最小长度 | ||||
|      */ | ||||
|     PASSWORD_MIN_LENGTH("密码最小长度为 %s 个字符", 8, 32) { | ||||
|     PASSWORD_MIN_LENGTH("密码最小长度取值范围为 %d-%d", 8, 32, "密码最小长度为 %d 个字符") { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|         public void validate(String password, int value, UserDO user) { | ||||
|             // 最小长度校验 | ||||
|             ValidationUtils.throwIf(StrUtil.length(password) < policyValue, this.getDescription() | ||||
|                 .formatted(policyValue)); | ||||
|             ValidationUtils.throwIf(StrUtil.length(password) < value, this.getMsg().formatted(value)); | ||||
|             // 完整校验 | ||||
|             int passwordMaxLength = this.getMax(); | ||||
|             ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE | ||||
|                 .formatted(policyValue, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", policyValue, passwordMaxLength); | ||||
|                 .formatted(value, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", value, passwordMaxLength); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码是否必须包含特殊字符 | ||||
|      */ | ||||
|     PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码必须包含特殊字符", SysConstants.NO, SysConstants.YES) { | ||||
|     PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码是否必须包含特殊字符取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码必须包含特殊字符") { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             ValidationUtils.throwIf(policyValue == SysConstants.YES && !ReUtil | ||||
|                 .isMatch(RegexConstants.SPECIAL_CHARACTER, password), this.getDescription()); | ||||
|         public void validateRange(int value, Map<String, String> policyMap) { | ||||
|             ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription() | ||||
|                 .formatted(SysConstants.YES, SysConstants.NO)); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void validate(String password, int value, UserDO user) { | ||||
|             ValidationUtils.throwIf(value == SysConstants.YES && !ReUtil | ||||
|                 .isMatch(RegexConstants.SPECIAL_CHARACTER, password), this.getMsg()); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码是否允许包含正反序账号名 | ||||
|      */ | ||||
|     PASSWORD_ALLOW_CONTAIN_USERNAME("密码不允许包含正反序账号名", SysConstants.NO, SysConstants.YES) { | ||||
|     PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含正反序账号名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序账号名") { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             if (policyValue <= SysConstants.NO) { | ||||
|         public void validateRange(int value, Map<String, String> policyMap) { | ||||
|             ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription() | ||||
|                 .formatted(SysConstants.YES, SysConstants.NO)); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void validate(String password, int value, UserDO user) { | ||||
|             if (value <= SysConstants.NO) { | ||||
|                 String username = user.getUsername(); | ||||
|                 ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil | ||||
|                     .reverse(username)), this.getDescription()); | ||||
|                     .reverse(username)), this.getMsg()); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| @@ -82,66 +134,63 @@ public enum PasswordPolicyEnum { | ||||
|     /** | ||||
|      * 密码重复使用规则 | ||||
|      */ | ||||
|     PASSWORD_REUSE_POLICY("不允许使用最近 %s 次的历史密码", 3, 32) { | ||||
|     PASSWORD_REUSE_POLICY("密码重复使用规则取值范围为 %d-%d", 3, 32, "不允许使用最近 %d 次的历史密码") { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|         public void validate(String password, int value, UserDO user) { | ||||
|             UserPasswordHistoryService userPasswordHistoryService = SpringUtil | ||||
|                 .getBean(UserPasswordHistoryService.class); | ||||
|             ValidationUtils.throwIf(userPasswordHistoryService.isPasswordReused(user | ||||
|                 .getId(), password, policyValue), this.getDescription().formatted(policyValue)); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的次数 | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_COUNT(null, SysConstants.NO, 10) { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             // 无需此处校验 | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的时间(min) | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_MINUTES(null, 1, 1440) { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             // 无需此处校验 | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码到期提前提示(天) | ||||
|      */ | ||||
|     PASSWORD_EXPIRATION_WARNING_DAYS(null, SysConstants.NO, Integer.MAX_VALUE) { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             // 无需此处校验 | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码有效期(天) | ||||
|      */ | ||||
|     PASSWORD_EXPIRATION_DAYS(null, SysConstants.NO, 999) { | ||||
|         @Override | ||||
|         public void validate(String password, int policyValue, UserDO user) { | ||||
|             // 无需此处校验 | ||||
|             ValidationUtils.throwIf(userPasswordHistoryService.isPasswordReused(user.getId(), password, value), this | ||||
|                 .getMsg() | ||||
|                 .formatted(value)); | ||||
|         } | ||||
|     },; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     private final String description; | ||||
|  | ||||
|     /** | ||||
|      * 最小值 | ||||
|      */ | ||||
|     private final Integer min; | ||||
|  | ||||
|     /** | ||||
|      * 最大值 | ||||
|      */ | ||||
|     private final Integer max; | ||||
|  | ||||
|     /** | ||||
|      * 提示信息 | ||||
|      */ | ||||
|     private final String msg; | ||||
|  | ||||
|     /** | ||||
|      * 策略前缀 | ||||
|      */ | ||||
|     public static final String PREFIX = "PASSWORD_"; | ||||
|  | ||||
|     /** | ||||
|      * 校验取值范围 | ||||
|      * | ||||
|      * @param value     值 | ||||
|      * @param policyMap 策略集合 | ||||
|      */ | ||||
|     public void validateRange(int value, Map<String, String> policyMap) { | ||||
|         Integer minValue = this.getMin(); | ||||
|         Integer maxValue = this.getMax(); | ||||
|         ValidationUtils.throwIf(value < minValue || value > maxValue, this.getDescription() | ||||
|             .formatted(minValue, maxValue)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验 | ||||
|      * | ||||
|      * @param password 密码 | ||||
|      * @param policyValue 策略值 | ||||
|      * @param value    策略值 | ||||
|      * @param user     用户信息 | ||||
|      */ | ||||
|     public abstract void validate(String password, int policyValue, UserDO user); | ||||
|     public void validate(String password, int value, UserDO user) { | ||||
|         // 无需校验 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -43,9 +43,9 @@ public interface OptionService { | ||||
|     /** | ||||
|      * 修改参数 | ||||
|      * | ||||
|      * @param req 参数信息 | ||||
|      * @param options 参数列表 | ||||
|      */ | ||||
|     void update(List<OptionReq> req); | ||||
|     void update(List<OptionReq> options); | ||||
|  | ||||
|     /** | ||||
|      * 重置参数 | ||||
|   | ||||
| @@ -17,10 +17,12 @@ | ||||
| package top.continew.admin.system.service.impl; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.util.NumberUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.system.enums.PasswordPolicyEnum; | ||||
| import top.continew.admin.system.mapper.OptionMapper; | ||||
| import top.continew.admin.system.model.entity.OptionDO; | ||||
| import top.continew.admin.system.model.query.OptionQuery; | ||||
| @@ -31,10 +33,13 @@ import top.continew.admin.system.service.OptionService; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.core.util.validate.CheckUtils; | ||||
| import top.continew.starter.core.util.validate.ValidationUtils; | ||||
| import top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.function.Function; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * 参数业务实现 | ||||
| @@ -54,9 +59,20 @@ public class OptionServiceImpl implements OptionService { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void update(List<OptionReq> req) { | ||||
|     public void update(List<OptionReq> options) { | ||||
|         Map<String, String> passwordPolicyOptionMap = options.stream() | ||||
|             .filter(option -> StrUtil.startWith(option.getCode(), PasswordPolicyEnum.PREFIX)) | ||||
|             .collect(Collectors.toMap(OptionReq::getCode, OptionReq::getValue, (oldVal, newVal) -> oldVal)); | ||||
|         // 校验密码策略参数取值范围 | ||||
|         for (Map.Entry<String, String> passwordPolicyOptionEntry : passwordPolicyOptionMap.entrySet()) { | ||||
|             String code = passwordPolicyOptionEntry.getKey(); | ||||
|             String value = passwordPolicyOptionEntry.getValue(); | ||||
|             ValidationUtils.throwIf(!NumberUtil.isNumber(value), "参数 [%s] 的值必须为数字", code); | ||||
|             PasswordPolicyEnum passwordPolicy = PasswordPolicyEnum.valueOf(code); | ||||
|             passwordPolicy.validateRange(Integer.parseInt(value), passwordPolicyOptionMap); | ||||
|         } | ||||
|         RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); | ||||
|         baseMapper.updateBatchById(BeanUtil.copyToList(req, OptionDO.class)); | ||||
|         baseMapper.updateBatchById(BeanUtil.copyToList(options, OptionDO.class)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -19,6 +19,7 @@ package top.continew.admin.webapi.system; | ||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| @@ -38,6 +39,7 @@ import java.util.List; | ||||
|  * @since 2023/8/26 19:38 | ||||
|  */ | ||||
| @Tag(name = "参数管理 API") | ||||
| @Validated | ||||
| @RestController | ||||
| @RequiredArgsConstructor | ||||
| @RequestMapping("/system/option") | ||||
| @@ -55,8 +57,8 @@ public class OptionController { | ||||
|     @Operation(summary = "修改参数", description = "修改参数") | ||||
|     @SaCheckPermission("system:config:update") | ||||
|     @PatchMapping | ||||
|     public R<Void> update(@Validated @RequestBody List<OptionReq> req) { | ||||
|         baseService.update(req); | ||||
|     public R<Void> update(@Valid @RequestBody List<OptionReq> options) { | ||||
|         baseService.update(options); | ||||
|         return R.ok(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -117,8 +117,8 @@ VALUES | ||||
| ('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ||||
| ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL), | ||||
| ('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL), | ||||
| ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||
| ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL), | ||||
| ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||
| ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), | ||||
| ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), | ||||
| ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL), | ||||
|   | ||||
| @@ -117,8 +117,8 @@ VALUES | ||||
| ('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ||||
| ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL), | ||||
| ('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL), | ||||
| ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||
| ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL), | ||||
| ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||
| ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), | ||||
| ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), | ||||
| ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user