feat: 系统配置新增安全设置功能

1、新增系统配置-安全设置CURD
2、用户个人修改密码时按照安全设置校验
3、密码连续错误账号锁定
4、密码过期判断
5、数据库数据初始化
This commit is contained in:
kils
2024-05-09 18:15:50 +08:00
committed by Charles7c
parent ad7412f9cb
commit 1de2a8f2dc
12 changed files with 247 additions and 16 deletions

View File

@@ -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;
/**
* 创建时间
*/

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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