Skip to content

Commit

Permalink
feat: 系统配置新增安全设置功能
Browse files Browse the repository at this point in the history
1、新增系统配置-安全设置CURD
2、用户个人修改密码时按照安全设置校验
3、密码连续错误账号锁定
4、密码过期判断
5、数据库数据初始化
  • Loading branch information
jskils authored and Charles7c committed May 9, 2024
1 parent ad7412f commit 1de2a8f
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class RegexConstants {
*/
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{6,32}$";

/**
* 密码正则严格版(长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符)
*/
public static final String PASSWORD_STRICT = "^\\S*(?=\\S{8,32})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*$";

/**
* 通用编码正则(长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* 创建时间
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
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;
Expand All @@ -32,6 +29,7 @@
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;
Expand All @@ -41,18 +39,21 @@
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;
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.*;

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;

/**
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,9 +34,11 @@
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;

/**
* 参数业务实现
Expand Down Expand Up @@ -61,4 +68,27 @@ public void resetValue(OptionResetValueReq req) {
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);
}

}
Loading

0 comments on commit 1de2a8f

Please sign in to comment.