refactor: 完善密码策略取值范围校验

This commit is contained in:
2024-05-18 14:44:56 +08:00
parent 857a1c9083
commit 5f5fee63f8
6 changed files with 136 additions and 69 deletions

View File

@@ -16,6 +16,9 @@
package top.continew.admin.system.enums; 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.ReUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; 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.RegexConstants;
import top.continew.admin.common.constant.SysConstants; import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.system.model.entity.UserDO; 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.admin.system.service.UserPasswordHistoryService;
import top.continew.starter.core.util.validate.ValidationUtils; 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 @RequiredArgsConstructor
public enum PasswordPolicyEnum { 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 @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() ValidationUtils.throwIf(StrUtil.length(password) < value, this.getMsg().formatted(value));
.formatted(policyValue));
// 完整校验 // 完整校验
int passwordMaxLength = this.getMax(); int passwordMaxLength = this.getMax();
ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE 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 @Override
public void validate(String password, int policyValue, UserDO user) { public void validateRange(int value, Map<String, String> policyMap) {
ValidationUtils.throwIf(policyValue == SysConstants.YES && !ReUtil ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription()
.isMatch(RegexConstants.SPECIAL_CHARACTER, password), 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 @Override
public void validate(String password, int policyValue, UserDO user) { public void validateRange(int value, Map<String, String> policyMap) {
if (policyValue <= SysConstants.NO) { 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(); String username = user.getUsername();
ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil 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 @Override
public void validate(String password, int policyValue, UserDO user) { public void validate(String password, int value, UserDO user) {
UserPasswordHistoryService userPasswordHistoryService = SpringUtil UserPasswordHistoryService userPasswordHistoryService = SpringUtil
.getBean(UserPasswordHistoryService.class); .getBean(UserPasswordHistoryService.class);
ValidationUtils.throwIf(userPasswordHistoryService.isPasswordReused(user ValidationUtils.throwIf(userPasswordHistoryService.isPasswordReused(user.getId(), password, value), this
.getId(), password, policyValue), this.getDescription().formatted(policyValue)); .getMsg()
} .formatted(value));
},
/**
* 登录密码错误锁定账号的次数
*/
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) {
// 无需此处校验
} }
},; },;
/**
* 描述
*/
private final String description; private final String description;
/**
* 最小值
*/
private final Integer min; private final Integer min;
/**
* 最大值
*/
private final Integer max; 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 password 密码
* @param policyValue 策略值 * @param value 策略值
* @param user 用户信息 * @param user 用户信息
*/ */
public abstract void validate(String password, int policyValue, UserDO user); public void validate(String password, int value, UserDO user) {
// 无需校验
}
} }

View File

@@ -43,9 +43,9 @@ public interface OptionService {
/** /**
* 修改参数 * 修改参数
* *
* @param req 参数信息 * @param options 参数列表
*/ */
void update(List<OptionReq> req); void update(List<OptionReq> options);
/** /**
* 重置参数 * 重置参数

View File

@@ -17,10 +17,12 @@
package top.continew.admin.system.service.impl; package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import top.continew.admin.common.constant.CacheConstants; 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.mapper.OptionMapper;
import top.continew.admin.system.model.entity.OptionDO; import top.continew.admin.system.model.entity.OptionDO;
import top.continew.admin.system.model.query.OptionQuery; 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.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.validate.CheckUtils; 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 top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* 参数业务实现 * 参数业务实现
@@ -54,9 +59,20 @@ public class OptionServiceImpl implements OptionService {
} }
@Override @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); RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK);
baseMapper.updateBatchById(BeanUtil.copyToList(req, OptionDO.class)); baseMapper.updateBatchById(BeanUtil.copyToList(options, OptionDO.class));
} }
@Override @Override

View File

@@ -19,6 +19,7 @@ package top.continew.admin.webapi.system;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -38,6 +39,7 @@ import java.util.List;
* @since 2023/8/26 19:38 * @since 2023/8/26 19:38
*/ */
@Tag(name = "参数管理 API") @Tag(name = "参数管理 API")
@Validated
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/system/option") @RequestMapping("/system/option")
@@ -55,8 +57,8 @@ public class OptionController {
@Operation(summary = "修改参数", description = "修改参数") @Operation(summary = "修改参数", description = "修改参数")
@SaCheckPermission("system:config:update") @SaCheckPermission("system:config:update")
@PatchMapping @PatchMapping
public R<Void> update(@Validated @RequestBody List<OptionReq> req) { public R<Void> update(@Valid @RequestBody List<OptionReq> options) {
baseService.update(req); baseService.update(options);
return R.ok(); return R.ok();
} }

View File

@@ -117,8 +117,8 @@ VALUES
('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), ('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL), ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL),
('登录密码错误锁定账号的时间min', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440一天', 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-9990 表示永久有效)。', NULL, NULL), ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-9990 表示永久有效)。', NULL, NULL),
('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示0 表示不提示)。', NULL, NULL),
('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL), ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL),

View File

@@ -117,8 +117,8 @@ VALUES
('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), ('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL), ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL),
('登录密码错误锁定账号的时间min', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440一天', 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-9990 表示永久有效)。', NULL, NULL), ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-9990 表示永久有效)。', NULL, NULL),
('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示0 表示不提示)。', NULL, NULL),
('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL), ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '', NULL, NULL),