refactor: 优化密码策略处理

This commit is contained in:
2024-05-15 23:14:51 +08:00
parent d44fb3a681
commit 90ecaab632
23 changed files with 303 additions and 270 deletions

View File

@@ -16,6 +16,7 @@
package top.continew.admin.auth.service;
import jakarta.servlet.http.HttpServletRequest;
import me.zhyd.oauth.model.AuthUser;
import top.continew.admin.auth.model.resp.RouteResp;
@@ -34,9 +35,10 @@ public interface LoginService {
*
* @param username 用户名
* @param password 密码
* @param request 请求对象
* @return 令牌
*/
String accountLogin(String username, String password);
String accountLogin(String username, String password, HttpServletRequest request);
/**
* 手机号登录

View File

@@ -21,7 +21,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.util.*;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -39,7 +41,7 @@ import top.continew.admin.common.enums.MessageTypeEnum;
import top.continew.admin.common.model.dto.LoginUser;
import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.enums.MessageTemplateEnum;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.enums.PasswordPolicyEnum;
import top.continew.admin.system.model.entity.DeptDO;
import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.UserDO;
@@ -80,36 +82,15 @@ public class LoginServiceImpl implements LoginService {
private final OptionService optionService;
@Override
public String accountLogin(String username, String password) {
public String accountLogin(String username, String password, HttpServletRequest request) {
UserDO user = userService.getByUsername(username);
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword());
isPasswordLocked(username, isError);
this.checkUserLocked(username, request, isError);
CheckUtils.throwIf(isError, "用户名或密码错误");
this.checkUserStatus(user);
return this.login(user);
}
/**
* 检测用户是否被密码锁定
*
* @param username 用户名
*/
private void isPasswordLocked(String username, boolean isError) {
// 不锁定账户
int maxErrorCount = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_ERROR_COUNT);
if (maxErrorCount <= 0) {
return;
}
int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES);
String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username;
long currentErrorCount = 0;
if (isError) {
currentErrorCount = RedisUtils.incr(key);
RedisUtils.expire(key, Duration.ofMinutes(lockMinutes));
}
CheckUtils.throwIf(currentErrorCount > maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes);
}
@Override
public String phoneLogin(String phone) {
UserDO user = userService.getByPhone(phone);
@@ -229,6 +210,36 @@ public class LoginServiceImpl implements LoginService {
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
}
/**
* 检测用户是否已被锁定
*
* @param username 用户名
* @param request 请求对象
* @param isError 是否登录错误
*/
private void checkUserLocked(String username, HttpServletRequest request, boolean isError) {
// 不锁定
int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name());
if (maxErrorCount <= SysConstants.NO) {
return;
}
// 检测是否已被锁定
String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil
.getClientIP(request));
int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name());
Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0);
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes);
// 登录成功清除计数
if (!isError) {
RedisUtils.delete(key);
return;
}
// 登录失败递增计数
currentErrorCount++;
RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes);
}
/**
* 发送系统消息
*
@@ -237,8 +248,8 @@ public class LoginServiceImpl implements LoginService {
private void sendSystemMsg(UserDO user) {
MessageReq req = new MessageReq();
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
req.setTitle(StrUtil.format(socialRegister.getTitle(), projectProperties.getName()));
req.setContent(StrUtil.format(socialRegister.getContent(), user.getNickname()));
req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
req.setContent(socialRegister.getContent().formatted(user.getNickname()));
req.setType(MessageTypeEnum.SYSTEM);
messageService.add(req, CollUtil.toList(user.getId()));
}

View File

@@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
/**
* 第三方登录
*/
SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。");
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");
private final String title;
private final String content;

View File

@@ -1,63 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 参数枚举
*
* @author Kils
* @since 2024/05/09 11:25
*/
@Getter
@RequiredArgsConstructor
public enum OptionCodeEnum {
/**
* 密码是否允许包含正反序帐户名
*/
PASSWORD_CONTAIN_NAME("password_contain_name", "密码不允许包含正反序帐户名"),
/**
* 密码错误锁定帐户次数
*/
PASSWORD_ERROR_COUNT("password_error_count", "密码错误锁定帐户次数"),
/**
* 密码有效期
*/
PASSWORD_EXPIRATION_DAYS("password_expiration_days", "密码有效期"),
/**
* 密码是否允许包含正反序帐户名
*/
PASSWORD_LOCK_MINUTES("password_lock_minutes", "密码错误锁定帐户的时间"),
/**
* 密码最小长度
*/
PASSWORD_MIN_LENGTH("password_min_length", "密码最小长度"),
/**
* 密码是否必须包含特殊字符
*/
PASSWORD_SPECIAL_CHAR("password_special_char", "密码是否必须包含特殊字符"),
/**
* 修改密码最短间隔
*/
PASSWORD_UPDATE_INTERVAL("password_update_interval", "修改密码最短间隔");
private final String value;
private final String description;
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.admin.common.constant.SysConstants;
/**
* 密码策略枚举
*
* @author Kils
* @author Charles7c
* @since 2024/5/9 11:25
*/
@Getter
@RequiredArgsConstructor
public enum PasswordPolicyEnum {
/**
* 登录密码错误锁定账号的次数
*/
PASSWORD_ERROR_LOCK_COUNT(null, SysConstants.NO, 10),
/**
* 登录密码错误锁定账号的时间min
*/
PASSWORD_ERROR_LOCK_MINUTES(null, 1, 1440),
/**
* 密码到期提前提示(天)
*/
PASSWORD_EXPIRATION_WARNING_DAYS(null, SysConstants.NO, Integer.MAX_VALUE),
/**
* 密码有效期(天)
*/
PASSWORD_EXPIRATION_DAYS(null, SysConstants.NO, 999),
/**
* 密码重复使用规则
*/
PASSWORD_REUSE_POLICY("不允许使用最近 %s 次的历史密码", 3, 32),
/**
* 密码最小长度
*/
PASSWORD_MIN_LENGTH("密码最小长度为 %s 个字符", 8, 32),
/**
* 密码是否允许包含正反序账号名
*/
PASSWORD_ALLOW_CONTAIN_USERNAME("密码不允许包含正反序账号名", SysConstants.NO, SysConstants.YES),
/**
* 密码是否必须包含特殊字符
*/
PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码必须包含特殊字符", SysConstants.NO, SysConstants.YES),;
private final String description;
private final Integer min;
private final Integer max;
}

View File

@@ -54,7 +54,7 @@ public class DeptReq extends BaseReq {
*/
@Schema(description = "名称", example = "测试部")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;
/**

View File

@@ -44,7 +44,7 @@ public class DictReq extends BaseReq {
*/
@Schema(description = "名称", example = "公告类型")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;
/**
@@ -52,7 +52,7 @@ public class DictReq extends BaseReq {
*/
@Schema(description = "编码", example = "notice_type")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;
/**

View File

@@ -48,7 +48,7 @@ public class RoleReq extends BaseReq {
*/
@Schema(description = "名称", example = "测试人员")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;
/**
@@ -56,7 +56,7 @@ public class RoleReq extends BaseReq {
*/
@Schema(description = "编码", example = "test")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;
/**

View File

@@ -55,7 +55,7 @@ public class StorageReq extends BaseReq {
*/
@Schema(description = "编码", example = "local")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;
/**

View File

@@ -45,7 +45,7 @@ public class UserBasicInfoUpdateReq implements Serializable {
*/
@Schema(description = "昵称", example = "张三")
@NotBlank(message = "昵称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String nickname;
/**

View File

@@ -51,7 +51,7 @@ public class UserReq extends BaseReq {
*/
@Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4 到 64 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字下划线,以字母开头")
private String username;
/**
@@ -59,7 +59,7 @@ public class UserReq extends BaseReq {
*/
@Schema(description = "昵称", example = "张三")
@NotBlank(message = "昵称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String nickname;
/**

View File

@@ -16,15 +16,14 @@
package top.continew.admin.system.service;
import java.util.List;
import java.util.function.Function;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.model.query.OptionQuery;
import top.continew.admin.system.model.req.OptionReq;
import top.continew.admin.system.model.req.OptionResetValueReq;
import top.continew.admin.system.model.resp.OptionResp;
import java.util.List;
import java.util.function.Function;
/**
* 参数业务接口
*
@@ -56,19 +55,19 @@ public interface OptionService {
void resetValue(OptionResetValueReq req);
/**
* 根据code获取int参数值
*
* @param code code
* @return 参数值
* 根据编码查询参数值
*
* @param code 编码
* @return 参数值(自动转换为 int 类型)
*/
int getValueByCode2Int(OptionCodeEnum code);
int getValueByCode2Int(String code);
/**
* 根据code获取参数值
*
* @param code code
* @param mapper 类型转换 ex:value -> Integer.parseInt(value)
* 根据编码查询参数值
*
* @param code 编码
* @param mapper 转换方法 e.g.value -> Integer.parseInt(value)
* @return 参数值
*/
<T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper);
}
<T> T getValueByCode(String code, Function<String, T> mapper);
}

View File

@@ -17,14 +17,10 @@
package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.mapper.OptionMapper;
import top.continew.admin.system.model.entity.OptionDO;
import top.continew.admin.system.model.query.OptionQuery;
@@ -70,25 +66,24 @@ public class OptionServiceImpl implements OptionService {
}
@Override
public int getValueByCode2Int(OptionCodeEnum code) {
public int getValueByCode2Int(String code) {
return this.getValueByCode(code, Integer::parseInt);
}
@Override
public <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper) {
String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue());
public <T> T getValueByCode(String code, Function<String, T> mapper) {
String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code);
if (StrUtil.isNotBlank(value)) {
return mapper.apply(value);
}
LambdaQueryWrapper<OptionDO> queryWrapper = Wrappers.<OptionDO>lambdaQuery()
.eq(OptionDO::getCode, code.getValue())
.select(OptionDO::getValue, OptionDO::getDefaultValue);
OptionDO optionDO = baseMapper.selectOne(queryWrapper);
CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code);
value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue());
CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code);
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value);
OptionDO option = baseMapper.lambdaQuery()
.eq(OptionDO::getCode, code)
.select(OptionDO::getValue, OptionDO::getDefaultValue)
.one();
CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code);
value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue());
CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code);
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value);
return mapper.apply(value);
}
}

View File

@@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
@@ -41,6 +40,7 @@ import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.mapper.UserMapper;
@@ -63,10 +63,13 @@ import top.continew.starter.extension.crud.service.CommonUserService;
import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;
import java.time.LocalDateTime;
import java.util.*;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static top.continew.admin.system.enums.OptionCodeEnum.*;
import static top.continew.admin.system.enums.PasswordPolicyEnum.*;
/**
* 用户业务实现
@@ -197,7 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误");
}
// 校验密码合法性
checkPassword(newPassword, user);
this.checkPassword(newPassword, user);
// 更新密码和密码重置时间
user.setPassword(newPassword);
user.setPwdResetTime(LocalDateTime.now());
@@ -205,52 +208,11 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
onlineUserService.cleanByUserId(user.getId());
}
/**
* 检测修改密码合法性
*
* @param password 密码
* @param user 用户
*/
private void checkPassword(String password, UserDO user) {
// 密码最小长度
int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH);
ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH
.getDescription() + "为 {}", passwordMinLength);
// 密码是否允许包含正反序用户名
int passwordContainName = optionService.getValueByCode2Int(PASSWORD_CONTAIN_NAME);
if (passwordContainName == 1) {
String username = user.getUsername();
ValidationUtils.throwIf(StrUtil.containsIgnoreCase(password, username) || StrUtil
.containsIgnoreCase(password, StrUtil.reverse(username)), PASSWORD_CONTAIN_NAME.getDescription());
}
// 密码是否必须包含特殊字符
int passwordSpecialChar = optionService.getValueByCode2Int(PASSWORD_SPECIAL_CHAR);
String match = RegexConstants.PASSWORD;
String desc = "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字";
if (passwordSpecialChar == 1) {
match = RegexConstants.PASSWORD_STRICT;
desc = "密码长度为 8 到 32 位包含至少1个大写字母、1个小写字母、1个数字1个特殊字符";
}
ValidationUtils.throwIf(!ReUtil.isMatch(match, password), desc);
// 密码修改间隔
if (ObjectUtil.isNull(user.getPwdResetTime())) {
return;
}
int passwordUpdateInterval = optionService.getValueByCode2Int(PASSWORD_UPDATE_INTERVAL);
if (passwordUpdateInterval <= 0) {
return;
}
LocalDateTime lastResetTime = user.getPwdResetTime();
LocalDateTime limitUpdateTime = lastResetTime.plusMinutes(passwordUpdateInterval);
ValidationUtils.throwIf(LocalDateTime.now().isBefore(limitUpdateTime), "上次修改于:{},下次可修改时间:{}", LocalDateTimeUtil
.formatNormal(lastResetTime), LocalDateTimeUtil.formatNormal(limitUpdateTime));
}
@Override
public Boolean isPasswordExpired(LocalDateTime pwdResetTime) {
// 永久有效
int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS);
if (passwordExpirationDays <= 0) {
int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name());
if (passwordExpirationDays <= SysConstants.NO) {
return false;
}
// 初始密码也提示修改
@@ -375,6 +337,33 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
userRoleService.add(req.getRoleIds(), userId);
}
/**
* 检测密码合法性
*
* @param password 密码
* @param user 用户信息
*/
private void checkPassword(String password, UserDO user) {
// 密码最小长度
int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH.name());
ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH.getDescription()
.formatted(passwordMinLength));
// 密码是否允许包含正反序账号名
int passwordAllowContainUsername = optionService.getValueByCode2Int(PASSWORD_ALLOW_CONTAIN_USERNAME.name());
if (passwordAllowContainUsername == SysConstants.NO) {
String username = user.getUsername();
ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil
.reverse(username)), PASSWORD_ALLOW_CONTAIN_USERNAME.getDescription());
}
int passwordMaxLength = PASSWORD_MIN_LENGTH.getMax();
ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE
.formatted(passwordMinLength, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", passwordMinLength, passwordMaxLength);
// 密码是否必须包含特殊字符
int passwordContainSpecialChar = optionService.getValueByCode2Int(PASSWORD_CONTAIN_SPECIAL_CHARACTERS.name());
ValidationUtils.throwIf(passwordContainSpecialChar == SysConstants.YES && !ReUtil
.isMatch(RegexConstants.SPECIAL_CHARACTER, password), PASSWORD_CONTAIN_SPECIAL_CHARACTERS.getDescription());
}
/**
* 名称是否存在
*