mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +08:00 
			
		
		
		
	feat: 系统配置新增安全设置功能
1、新增系统配置-安全设置CURD 2、用户个人修改密码时按照安全设置校验 3、密码连续错误账号锁定 4、密码过期判断 5、数据库数据初始化
This commit is contained in:
		| @@ -98,6 +98,12 @@ public class UserInfoResp implements Serializable { | ||||
|     @Schema(description = "最后一次修改密码时间", example = "2023-08-08 08:08:08", type = "string") | ||||
|     private LocalDateTime pwdResetTime; | ||||
|  | ||||
|     /** | ||||
|      * 密码是否已过期 | ||||
|      */ | ||||
|     @Schema(description = "密码是否已过期", example = "true") | ||||
|     private Boolean passwordExpired; | ||||
|  | ||||
|     /** | ||||
|      * 创建时间 | ||||
|      */ | ||||
|   | ||||
| @@ -20,10 +20,7 @@ import cn.hutool.core.bean.BeanUtil; | ||||
| 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.IdUtil; | ||||
| import cn.hutool.core.util.RandomUtil; | ||||
| import cn.hutool.core.util.ReUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.core.util.*; | ||||
| import cn.hutool.json.JSONUtil; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| @@ -32,6 +29,7 @@ import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.auth.model.resp.RouteResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.auth.service.PermissionService; | ||||
| 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; | ||||
| @@ -41,6 +39,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.model.entity.DeptDO; | ||||
| import top.continew.admin.system.model.entity.RoleDO; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| @@ -48,11 +47,13 @@ import top.continew.admin.system.model.entity.UserSocialDO; | ||||
| import top.continew.admin.system.model.req.MessageReq; | ||||
| import top.continew.admin.system.model.resp.MenuResp; | ||||
| import top.continew.admin.system.service.*; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.autoconfigure.project.ProjectProperties; | ||||
| import top.continew.starter.core.util.validate.CheckUtils; | ||||
| import top.continew.starter.extension.crud.annotation.TreeField; | ||||
| import top.continew.starter.extension.crud.util.TreeUtils; | ||||
|  | ||||
| import java.time.Duration; | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.*; | ||||
|  | ||||
| @@ -76,16 +77,41 @@ public class LoginServiceImpl implements LoginService { | ||||
|     private final UserSocialService userSocialService; | ||||
|     private final MessageService messageService; | ||||
|     private final PasswordEncoder passwordEncoder; | ||||
|     private final OptionService optionService; | ||||
|  | ||||
|     @Override | ||||
|     public String accountLogin(String username, String password) { | ||||
|         UserDO user = userService.getByUsername(username); | ||||
|         CheckUtils.throwIfNull(user, "用户名或密码不正确"); | ||||
|         CheckUtils.throwIf(!passwordEncoder.matches(password, user.getPassword()), "用户名或密码不正确"); | ||||
|         boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword()); | ||||
|         isPasswordLocked(username, 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; | ||||
|         } | ||||
|         String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username; | ||||
|         Long currentErrorCount = RedisUtils.get(key); | ||||
|         currentErrorCount = currentErrorCount == null ? 0 : currentErrorCount; | ||||
|         int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES); | ||||
|         if (isError) { | ||||
|             // 密码错误自增次数,并重置时间 | ||||
|             currentErrorCount = currentErrorCount + 1; | ||||
|             RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); | ||||
|         } | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String phoneLogin(String phone) { | ||||
|         UserDO user = userService.getByPhone(phone); | ||||
|   | ||||
| @@ -0,0 +1,63 @@ | ||||
| /* | ||||
|  * 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; | ||||
| } | ||||
| @@ -17,7 +17,9 @@ | ||||
| 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; | ||||
| @@ -52,4 +54,21 @@ public interface OptionService { | ||||
|      * @param req 重置信息 | ||||
|      */ | ||||
|     void resetValue(OptionResetValueReq req); | ||||
|  | ||||
|     /** | ||||
|      * 根据code获取int参数值 | ||||
|      *  | ||||
|      * @param code code | ||||
|      * @return 参数值 | ||||
|      */ | ||||
|     int getValueByCode2Int(OptionCodeEnum code); | ||||
|  | ||||
|     /** | ||||
|      * 根据code获取参数值 | ||||
|      *  | ||||
|      * @param code   code | ||||
|      * @param mapper 类型转换 ex:value -> Integer.parseInt(value) | ||||
|      * @return 参数值 | ||||
|      */ | ||||
|     <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper); | ||||
| } | ||||
| @@ -28,6 +28,7 @@ import top.continew.admin.system.model.resp.UserResp; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
| import top.continew.starter.data.mybatis.plus.service.IService; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
| @@ -137,4 +138,12 @@ public interface UserService extends BaseService<UserResp, UserDetailResp, UserQ | ||||
|      * @return 用户数量 | ||||
|      */ | ||||
|     Long countByDeptIds(List<Long> deptIds); | ||||
|  | ||||
|     /** | ||||
|      * 密码是否已过期 | ||||
|      *  | ||||
|      * @param pwdResetTime 上次重置密码时间 | ||||
|      * @return 是否过期 | ||||
|      */ | ||||
|     Boolean isPasswordExpired(LocalDateTime pwdResetTime); | ||||
| } | ||||
|   | ||||
| @@ -17,9 +17,14 @@ | ||||
| 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; | ||||
| @@ -29,9 +34,11 @@ import top.continew.admin.system.model.resp.OptionResp; | ||||
| 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.data.mybatis.plus.query.QueryWrapperHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.function.Function; | ||||
|  | ||||
| /** | ||||
|  * 参数业务实现 | ||||
| @@ -61,4 +68,27 @@ public class OptionServiceImpl implements OptionService { | ||||
|         RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); | ||||
|         baseMapper.lambdaUpdate().set(OptionDO::getValue, null).in(OptionDO::getCode, req.getCode()).update(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getValueByCode2Int(OptionCodeEnum 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()); | ||||
|         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); | ||||
|         return mapper.apply(value); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -18,8 +18,10 @@ 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; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import com.alicp.jetcache.anno.CacheInvalidate; | ||||
| import com.alicp.jetcache.anno.CacheType; | ||||
| @@ -38,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; | ||||
| 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.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.common.util.helper.LoginHelper; | ||||
| import top.continew.admin.system.mapper.UserMapper; | ||||
| @@ -53,18 +56,18 @@ import top.continew.admin.system.model.resp.UserResp; | ||||
| import top.continew.admin.system.service.*; | ||||
| 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.extension.crud.model.query.PageQuery; | ||||
| import top.continew.starter.extension.crud.model.resp.PageResp; | ||||
| import top.continew.starter.extension.crud.service.CommonUserService; | ||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.Collection; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.*; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import static top.continew.admin.system.enums.OptionCodeEnum.*; | ||||
|  | ||||
| /** | ||||
|  * 用户业务实现 | ||||
|  * | ||||
| @@ -81,6 +84,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | ||||
|     private final FileService fileService; | ||||
|     private final FileStorageService fileStorageService; | ||||
|     private final PasswordEncoder passwordEncoder; | ||||
|     private final OptionService optionService; | ||||
|     @Resource | ||||
|     private DeptService deptService; | ||||
|     @Value("${avatar.support-suffix}") | ||||
| @@ -193,10 +197,68 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | ||||
|         if (StrUtil.isNotBlank(password)) { | ||||
|             CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误"); | ||||
|         } | ||||
|         // 校验密码合法性 | ||||
|         checkPassword(newPassword, user); | ||||
|         // 更新密码和密码重置时间 | ||||
|         user.setPassword(newPassword); | ||||
|         user.setPwdResetTime(LocalDateTime.now()); | ||||
|         baseMapper.updateById(user); | ||||
|         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) { | ||||
|             return false; | ||||
|         } | ||||
|         // 初始密码也提示修改 | ||||
|         if (pwdResetTime == null) { | ||||
|             return true; | ||||
|         } | ||||
|         return pwdResetTime.plusDays(passwordExpirationDays).isBefore(LocalDateTime.now()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 kils
					kils