mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-09 20:57:21 +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