v3.6.0 三级等保重磅更新:1、【新增】双因子方式登录;2、【新增】定期修改密码;3、【新增】最大活跃时间;4、【新增】敏感数据脱敏;5、【新增】登录锁定配置;6、【新增】密码复杂度配置;7、【新增】三级等保可配置

This commit is contained in:
zhuoda
2024-09-02 22:20:29 +08:00
parent ac7c9940bf
commit 92dddd507b
80 changed files with 2770 additions and 501 deletions

View File

@@ -0,0 +1,59 @@
package net.lab1024.sa.base.common.json.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import net.lab1024.sa.base.module.support.datamasking.DataMasking;
import net.lab1024.sa.base.module.support.datamasking.DataMaskingTypeEnum;
import net.lab1024.sa.base.module.support.datamasking.SmartDataMaskingUtil;
import org.apache.commons.lang3.ObjectUtils;
import java.io.IOException;
/**
* 脱敏序列化
*
* @author 罗伊
* @description:
* @date 2024/7/21 4:39 下午
*/
public class DataMaskingSerializer extends JsonSerializer<Object> implements ContextualSerializer {
private DataMaskingTypeEnum typeEnum;
@Override
public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
if (ObjectUtils.isEmpty(value)) {
jsonGenerator.writeObject(value);
return;
}
if (typeEnum == null) {
jsonGenerator.writeObject(SmartDataMaskingUtil.dataMasking(String.valueOf(value)));
return;
}
jsonGenerator.writeObject(SmartDataMaskingUtil.dataMasking(value, typeEnum));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
// 判断beanProperty是不是空
if (null == property) {
return prov.findNullValueSerializer(property);
}
DataMasking annotation = property.getAnnotation(DataMasking.class);
if (null == annotation) {
return prov.findValueSerializer(property.getType(), property);
}
typeEnum = annotation.value();
return this;
}
}

View File

@@ -28,7 +28,7 @@ import java.util.Optional;
* springdoc-openapi 配置
* nginx配置前缀时如果需要访问【/swagger-ui/index.html】需添加额外nginx配置
* location /v3/api-docs/ {
* proxy_pass http://127.0.0.1:11024/v3/api-docs/;
* proxy_pass http://127.0.0.1:1024/v3/api-docs/;
* }
* @Author 1024创新实验室-主任: 卓大
* @Date 2020-03-25 22:54:46
@@ -43,7 +43,7 @@ public class SwaggerConfig {
/**
* 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题
*/
@Value("${springdoc.swagger-ui.server-base-url:''}")
@Value("${springdoc.swagger-ui.server-base-url}")
private String serverBaseUrl;
public static final String[] SWAGGER_WHITELIST = {

View File

@@ -23,5 +23,7 @@ public class RedisKeyConst {
public static final String CAPTCHA = "captcha:";
public static final String LOGIN_VERIFICATION_CODE = "login:verification-code:";
}
}

View File

@@ -49,6 +49,8 @@ public class SwaggerTagConst {
public static final String PROTECT = "业务支撑-网络安全";
public static final String DATA_MASKING = "业务支撑-数据脱敏";
public static final String JOB = "业务支撑-定时任务";
public static final String MESSAGE = "业务支撑-消息";

View File

@@ -42,8 +42,10 @@ public class CaptchaService {
@Resource
private DefaultKaptcha defaultKaptcha;
@Autowired
@Resource
private SystemEnvironment systemEnvironment;
@Resource
private RedisService redisService;

View File

@@ -22,6 +22,7 @@ public enum ConfigKeyEnum implements BaseEnum {
*/
SUPER_PASSWORD("super_password", "万能密码"),
LEVEL3_PROTECT_CONFIG("level3_protect_config", "三级等保配置"),
;
private final String value;

View File

@@ -109,7 +109,8 @@ public class ConfigService {
*
*/
public String getConfigValue(ConfigKeyEnum configKey) {
return this.getConfig(configKey).getConfigValue();
ConfigVO config = this.getConfig(configKey);
return config == null ? null : config.getConfigValue();
}
/**
@@ -125,12 +126,12 @@ public class ConfigService {
* 添加系统配置
*
*/
public ResponseDTO<String> add(ConfigAddForm configAddDTO) {
ConfigEntity entity = configDao.selectByKey(configAddDTO.getConfigKey());
public ResponseDTO<String> add(ConfigAddForm configAddForm) {
ConfigEntity entity = configDao.selectByKey(configAddForm.getConfigKey());
if (null != entity) {
return ResponseDTO.error(UserErrorCode.ALREADY_EXIST);
}
entity = SmartBeanUtil.copy(configAddDTO, ConfigEntity.class);
entity = SmartBeanUtil.copy(configAddForm, ConfigEntity.class);
configDao.insert(entity);
// 刷新缓存

View File

@@ -0,0 +1,27 @@
package net.lab1024.sa.base.module.support.datamasking;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import net.lab1024.sa.base.common.json.serializer.DataMaskingSerializer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 脱敏注解
*
* @author 罗伊
* @description:
* @date 2024/7/21 4:39 下午
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerializer.class, nullsUsing = DataMaskingSerializer.class)
public @interface DataMasking {
DataMaskingTypeEnum value() default DataMaskingTypeEnum.COMMON;
}

View File

@@ -0,0 +1,40 @@
package net.lab1024.sa.base.module.support.datamasking;
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 脱敏数据类型
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/1
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@AllArgsConstructor
@Getter
public enum DataMaskingTypeEnum {
COMMON(null, "通用"),
PHONE(DesensitizedUtil.DesensitizedType.MOBILE_PHONE, "手机号"),
CHINESE_NAME(DesensitizedUtil.DesensitizedType.CHINESE_NAME, "中文名"),
ID_CARD(DesensitizedUtil.DesensitizedType.ID_CARD, "身份证号"),
FIXED_PHONE(DesensitizedUtil.DesensitizedType.FIXED_PHONE, "座机号"),
ADDRESS(DesensitizedUtil.DesensitizedType.ADDRESS, "地址"),
EMAIL(DesensitizedUtil.DesensitizedType.EMAIL, "电子邮件"),
PASSWORD(DesensitizedUtil.DesensitizedType.PASSWORD, "密码"),
CAR_LICENSE(DesensitizedUtil.DesensitizedType.CAR_LICENSE, "中国大陆车牌"),
BANK_CARD(DesensitizedUtil.DesensitizedType.BANK_CARD, "银行卡"),
USER_ID(DesensitizedUtil.DesensitizedType.USER_ID, "用户id");
private DesensitizedUtil.DesensitizedType type;
private String desc;
}

View File

@@ -0,0 +1,216 @@
package net.lab1024.sa.base.module.support.datamasking;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 脱敏工具类
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2024-07-23 21:38:52
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class SmartDataMaskingUtil {
/**
* 类 加注解字段缓存
*/
private static final ConcurrentHashMap<Class<?>, List<Field>> fieldMap = new ConcurrentHashMap<>();
public static String dataMasking(String value) {
if (StringUtils.isBlank(value)) {
return value;
}
if (value.length() < 4) {
return StrUtil.hide(value, 0, value.length());
}
int valueLength = value.length();
int startHideIndex = getHideStartIndex(valueLength);
int endHideIndex = getHideEndIndex(valueLength);
return StrUtil.hide(value, startHideIndex, endHideIndex);
}
public static Object dataMasking(Object value, DataMaskingTypeEnum dataType) {
if (value == null) {
return null;
}
if (dataType == null) {
return dataMasking(String.valueOf(value));
}
switch (dataType) {
case PHONE:
return DesensitizedUtil.mobilePhone(String.valueOf(value));
case CHINESE_NAME:
return DesensitizedUtil.chineseName(String.valueOf(value));
case ID_CARD:
return DesensitizedUtil.idCardNum(String.valueOf(value), 6, 2);
case FIXED_PHONE:
return DesensitizedUtil.fixedPhone(String.valueOf(value));
case ADDRESS:
return StrUtil.hide(String.valueOf(value), 6, String.valueOf(value).length() - 1);
case EMAIL:
return DesensitizedUtil.email(String.valueOf(value));
case PASSWORD:
return DesensitizedUtil.password(String.valueOf(value));
case CAR_LICENSE:
return DesensitizedUtil.carLicense(String.valueOf(value));
case BANK_CARD:
return DesensitizedUtil.bankCard(String.valueOf(value));
case USER_ID:
return DesensitizedUtil.userId();
default:
return dataMasking(String.valueOf(value));
}
}
/**
* 批量脱敏
*/
public static <T> void dataMasking(Collection<T> objectList) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
if (CollectionUtils.isEmpty(objectList)) {
return;
}
for (T object : objectList) {
dataMasking(object);
}
}
/**
* 单个脱敏
*/
public static <T> void dataMasking(T object) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
Class<?> tClass = object.getClass();
List<Field> fieldList = getField(object);
for (Field field : fieldList) {
field.setAccessible(true);
String fieldValue = "";
try {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), tClass);
Method getMethod = pd.getReadMethod();
Object value = getMethod.invoke(object);
if (value != null) {
fieldValue = value.toString();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
if (StringUtils.isBlank(fieldValue)) {
continue;
}
int valueLength = fieldValue.length();
int startHideIndex = getHideStartIndex(valueLength);
int endHideIndex = getHideEndIndex(valueLength);
try {
field.set(object, StrUtil.hide(fieldValue, startHideIndex, endHideIndex));
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
}
private static int getHideStartIndex(int totalLength) {
if (totalLength <= 4) {
return 1;
} else if (totalLength <= 6) {
return 1;
} else if (totalLength <= 10) {
return 2;
} else if (totalLength <= 18) {
return 3;
} else if (totalLength <= 27) {
return 5;
} else if (totalLength <= 34) {
return 7;
} else if (totalLength <= 41) {
return 9;
} else {
return 15;
}
}
private static int getHideEndIndex(int totalLength) {
if (totalLength <= 4) {
return totalLength - 1;
} else if (totalLength <= 6) {
return totalLength - 2;
} else if (totalLength <= 10) {
return totalLength - 2;
} else if (totalLength <= 18) {
return totalLength - 4;
} else if (totalLength <= 27) {
return totalLength - 6;
} else if (totalLength <= 34) {
return totalLength - 8;
} else if (totalLength <= 41) {
return totalLength - 10;
} else {
return totalLength - 16;
}
}
public static List<Field> getField(Object object) throws IntrospectionException {
// 从缓存中查询
Class<?> tClass = object.getClass();
List<Field> fieldList = fieldMap.get(tClass);
if (null != fieldList) {
return fieldList;
}
// 这一段递归代码 是为了 从父类中获取属性
Class<?> tempClass = tClass;
fieldList = new ArrayList<>();
while (tempClass != null) {
Field[] declaredFields = tempClass.getDeclaredFields();
for (Field field : declaredFields) {
boolean stringField = false;
try {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), tClass);
Method getMethod = pd.getReadMethod();
Type returnType = getMethod.getGenericReturnType();
stringField = "java.lang.String".equals(returnType.getTypeName());
} catch (Exception e) {
throw new RuntimeException(e);
}
if (field.isAnnotationPresent(DataMasking.class) && stringField) {
field.setAccessible(true);
fieldList.add(field);
}
}
tempClass = tempClass.getSuperclass();
}
fieldMap.put(tClass, fieldList);
return fieldList;
}
public static void main(String[] args) {
System.out.println(dataMasking("a", null));
System.out.println(dataMasking("ab", null));
System.out.println(dataMasking("abc", null));
System.out.println(dataMasking("abcd", null));
System.out.println(dataMasking("abcde", null));
}
}

View File

@@ -19,9 +19,9 @@ import net.lab1024.sa.base.module.support.file.domain.vo.FileDownloadVO;
import net.lab1024.sa.base.module.support.file.domain.vo.FileUploadVO;
import net.lab1024.sa.base.module.support.file.domain.vo.FileVO;
import net.lab1024.sa.base.module.support.redis.RedisService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityFileService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@@ -39,7 +39,7 @@ import java.util.stream.Collectors;
* @Date 2019年10月11日 15:34:47
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service
public class FileService {
@@ -58,9 +58,8 @@ public class FileService {
@Resource
private RedisService redisService;
@Value("${spring.servlet.multipart.max-file-size}")
private String maxFileSize;
@Resource
private SecurityFileService securityFileService;
/**
* 文件上传服务
@@ -89,11 +88,10 @@ public class FileService {
return ResponseDTO.userErrorParam("文件名称最大长度为:" + FILE_NAME_MAX_LENGTH);
}
// 校验文件大小
String maxSizeStr = maxFileSize.toLowerCase().replace("mb", "");
long maxSize = Integer.parseInt(maxSizeStr) * 1024 * 1024L;
if (file.getSize() > maxSize) {
return ResponseDTO.userErrorParam("上传文件最大为:" + maxSize);
// 校验文件大小以及安全性
ResponseDTO<String> validateFile = securityFileService.checkFile(file);
if (!validateFile.getOk()) {
return ResponseDTO.error(validateFile);
}
// 进行上传
@@ -192,7 +190,7 @@ public class FileService {
// 根据文件服务类 获取对应文件服务 查询 url
ResponseDTO<FileDownloadVO> download = fileStorageService.download(fileKey);
if(download.getOk()){
if (download.getOk()) {
download.getData().getMetadata().setFileName(fileVO.getFileName());
}
return download;

View File

@@ -0,0 +1,179 @@
package net.lab1024.sa.base.module.support.mail;
import cn.hutool.core.lang.UUID;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.domain.SystemEnvironment;
import net.lab1024.sa.base.module.support.mail.constant.MailTemplateCodeEnum;
import net.lab1024.sa.base.module.support.mail.constant.MailTemplateTypeEnum;
import net.lab1024.sa.base.module.support.mail.domain.MailTemplateEntity;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.StringWriter;
import java.io.Writer;
import java.util.List;
import java.util.Map;
/**
*
* 发生邮件:<br/>
* 1、支持直接发送 <br/>
* 2、支持使用邮件模板发送
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/5
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Slf4j
@Component
public class MailService {
@Autowired
private JavaMailSender javaMailSender;
@Resource
private MailTemplateDao mailTemplateDao;
@Resource
private SystemEnvironment systemEnvironment;
@Value("${spring.mail.username}")
private String clientMail;
/**
* 使用模板发送邮件
*/
public ResponseDTO<String> sendMail(MailTemplateCodeEnum templateCode, Map<String, Object> templateParamsMap, List<String> receiverUserList, List<File> fileList) {
MailTemplateEntity mailTemplateEntity = mailTemplateDao.selectById(templateCode.name().toLowerCase());
if (mailTemplateEntity == null) {
return ResponseDTO.userErrorParam("模版不存在");
}
if (mailTemplateEntity.getDisableFlag()) {
return ResponseDTO.userErrorParam("模版已禁用,无法发送");
}
String content = null;
if (MailTemplateTypeEnum.FREEMARKER.name().equalsIgnoreCase(mailTemplateEntity.getTemplateType().trim())) {
content = freemarkerResolverContent(mailTemplateEntity.getTemplateContent(), templateParamsMap);
} else if (MailTemplateTypeEnum.STRING.name().equalsIgnoreCase(mailTemplateEntity.getTemplateType().trim())) {
content = stringResolverContent(mailTemplateEntity.getTemplateContent(), templateParamsMap);
} else {
return ResponseDTO.userErrorParam("模版类型不存在");
}
try {
this.sendMail(mailTemplateEntity.getTemplateSubject(), content, fileList, receiverUserList, true);
} catch (Throwable e) {
log.error("邮件发送失败", e);
return ResponseDTO.userErrorParam("邮件发送失败");
}
return ResponseDTO.ok();
}
/**
* 使用模板发送邮件
*/
public ResponseDTO<String> sendMail(MailTemplateCodeEnum templateCode, Map<String, Object> templateParamsMap, List<String> receiverUserList) {
return this.sendMail(templateCode, templateParamsMap, receiverUserList, null);
}
/**
* 发送邮件
*
* @param subject 主题
* @param content 内容
* @param fileList 文件
* @param receiverUserList 接收方
* @throws MessagingException
*/
public void sendMail(String subject, String content, List<File> fileList, List<String> receiverUserList, boolean isHtml) throws MessagingException {
if (CollectionUtils.isEmpty(receiverUserList)) {
throw new RuntimeException("接收方不能为空");
}
if (StringUtils.isBlank(content)) {
throw new RuntimeException("邮件内容不能为空");
}
if (!systemEnvironment.isProd()) {
subject = "(测试)" + subject;
}
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
//是否为多文件上传
boolean multiparty = !CollectionUtils.isEmpty(fileList);
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, multiparty);
helper.setFrom(clientMail);
helper.setTo(receiverUserList.toArray(new String[0]));
helper.setSubject(subject);
//发送html格式
helper.setText(content, isHtml);
//附件
if (multiparty) {
for (File file : fileList) {
helper.addAttachment(file.getName(), file);
}
}
javaMailSender.send(mimeMessage);
}
/**
* 使用字符串生成最终内容
*/
private String stringResolverContent(String stringTemplate, Map<String, Object> templateParamsMap) {
StringSubstitutor stringSubstitutor = new StringSubstitutor(templateParamsMap);
String contractHtml = stringSubstitutor.replace(stringTemplate);
Document doc = Jsoup.parse(contractHtml);
doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
return doc.outerHtml();
}
/**
* 使用 freemarker 生成最终内容
*/
private String freemarkerResolverContent(String htmlTemplate, Map<String, Object> templateParamsMap) {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
StringTemplateLoader stringLoader = new StringTemplateLoader();
String templateName = UUID.fastUUID().toString(true);
stringLoader.putTemplate(templateName, htmlTemplate);
configuration.setTemplateLoader(stringLoader);
try {
Template template = configuration.getTemplate(templateName, "utf-8");
Writer out = new StringWriter(2048);
template.process(templateParamsMap, out);
return out.toString();
} catch (Throwable e) {
log.error("freemarkerResolverContent error: ", e);
}
return "";
}
}

View File

@@ -0,0 +1,22 @@
package net.lab1024.sa.base.module.support.mail;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.lab1024.sa.base.module.support.mail.domain.MailTemplateEntity;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;
/**
* 邮件模板
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/5
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Mapper
@Component
public interface MailTemplateDao extends BaseMapper<MailTemplateEntity> {
}

View File

@@ -0,0 +1,19 @@
package net.lab1024.sa.base.module.support.mail.constant;
/**
* 模版编码
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/5
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
public enum MailTemplateCodeEnum {
/**
* 登录验证码
*/
LOGIN_VERIFICATION_CODE
}

View File

@@ -0,0 +1,30 @@
package net.lab1024.sa.base.module.support.mail.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
/**
* 邮件模板类型
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/5
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Getter
@AllArgsConstructor
public enum MailTemplateTypeEnum implements BaseEnum {
STRING("string", "字符串替代器"),
FREEMARKER("freemarker", "freemarker模板引擎");
private String value;
private String desc;
}

View File

@@ -0,0 +1,51 @@
package net.lab1024.sa.base.module.support.mail.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
*
* 邮件模板
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/8/5
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Data
@TableName("t_mail_template")
public class MailTemplateEntity {
@TableId(type = IdType.NONE)
private String templateCode;
/**
* 主题
*/
private String templateSubject;
/**
* 模板类型
*/
private String templateType;
/**
* 模板内容
*/
private String templateContent;
/**
* 禁用标识
*/
private Boolean disableFlag;
private LocalDateTime updateTime;
private LocalDateTime createTime;
}

View File

@@ -22,6 +22,12 @@ public class OperateLogQueryForm extends PageParam {
@Schema(description = "用户类型")
private Integer operateUserType;
@Schema(description = "关键字:模块、操作内容")
private String keywords;
@Schema(description = "请求关键字:请求地址、请求方法、请求参数")
private String requestKeywords;
@Schema(description = "开始日期")
private String startDate;

View File

@@ -0,0 +1,34 @@
package net.lab1024.sa.base.module.support.securityprotect.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.lab1024.sa.base.module.support.securityprotect.domain.PasswordLogEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
@Mapper
@Component
public interface PasswordLogDao extends BaseMapper<PasswordLogEntity> {
/**
* 查询最后一次修改密码记录
*
* @param userType
* @param userId
* @return
*/
PasswordLogEntity selectLastByUserTypeAndUserId(@Param("userType") Integer userType, @Param("userId") Long userId);
/**
* 查询最近几次修改后的密码
*
* @param userType
* @param userId
* @return
*/
List<String> selectOldPassword(@Param("userType") Integer userType, @Param("userId") Long userId, @Param("limit") int limit);
}

View File

@@ -0,0 +1,58 @@
package net.lab1024.sa.base.module.support.securityprotect.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 三级等保相关配置
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/7/30
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Data
public class Level3ProtectConfigForm {
@Schema(description = "连续登录失败次数则锁定")
@NotNull(message = "连续登录失败次数则锁定 不能为空")
private Integer loginFailMaxTimes;
@Schema(description = "连续登录失败锁定时间(单位:分钟)")
@NotNull(message = "连续登录失败锁定时间(单位:分钟) 不能为空")
private Integer loginFailLockMinutes;
@Schema(description = "最低活跃时间(单位:分钟)")
@NotNull(message = "最低活跃时间(单位:分钟) 不能为空")
private Integer loginActiveTimeoutMinutes;
@Schema(description = "开启双因子登录")
@NotNull(message = "开启双因子登录 不能为空")
private Boolean twoFactorLoginEnabled;
@Schema(description = "密码复杂度 是否开启,默认:开启")
@NotNull(message = "密码复杂度 是否开启 不能为空")
private Boolean passwordComplexityEnabled;
@Schema(description = "定期修改密码时间间隔(默认:月)")
@NotNull(message = "定期修改密码时间间隔(默认:月) 不能为空")
private Integer regularChangePasswordMonths;
@Schema(description = "定期修改密码不允许重复次数默认3次以内密码不能相同默认")
@NotNull(message = "定期修改密码不允许重复次数 不能为空")
private Integer regularChangePasswordNotAllowRepeatTimes;
@Schema(description = "文件检测,默认:不开启")
@NotNull(message = "文件检测 是否开启 不能为空")
private Boolean fileDetectFlag;
@Schema(description = "文件大小限制,单位 mb (默认50 mb)")
@NotNull(message = "文件大小限制 不能为空")
private Long maxUploadFileSizeMb;
}

View File

@@ -0,0 +1,43 @@
package net.lab1024.sa.base.module.support.securityprotect.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author yandy
* @description:
* @date 2024/7/15 1:39 下午
*/
@Data
@TableName("t_password_log")
public class PasswordLogEntity {
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Long id;
private Integer userType;
private Long userId;
private String oldPassword;
private String newPassword;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,207 @@
package net.lab1024.sa.base.module.support.securityprotect.service;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.module.support.config.ConfigKeyEnum;
import net.lab1024.sa.base.module.support.config.ConfigService;
import net.lab1024.sa.base.module.support.securityprotect.domain.Level3ProtectConfigForm;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* 三级等保配置
*
* @Author 1024创新实验室-创始人兼主任:卓大
* @Date 2024/7/30
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> Since 2012
*/
@Service
@Slf4j
public class Level3ProtectConfigService {
/**
* 开启双因子登录,默认:开启
*/
private boolean twoFactorLoginEnabled = false;
/**
* 连续登录失败次数则锁定,-1表示不受限制可以一直尝试登录
*/
private int loginFailMaxTimes = -1;
/**
* 连续登录失败锁定时间(单位:秒),-1表示不锁定建议锁定30分钟
*/
private int loginFailLockSeconds = 1800;
/**
* 最低活跃时间(单位:秒),超过此时间没有操作系统就会被冻结,默认-1 代表不限制,永不冻结; 默认 30分钟
*/
private int loginActiveTimeoutSeconds = 1800;
/**
* 密码复杂度 是否开启,默认:开启
*/
private boolean passwordComplexityEnabled = true;
/**
* 定期修改密码时间间隔默认默认建议90天更换密码
*/
private int regularChangePasswordDays = 90;
/**
* 定期修改密码不允许相同次数默认3次以内密码不能相同
*/
private int regularChangePasswordNotAllowRepeatTimes = 3;
/**
* 文件大小限制,单位 mb (默认50 mb)
*/
private long maxUploadFileSizeMb = 50;
/**
* 文件检测,默认:不开启
*/
private boolean fileDetectFlag = false;
@Resource
private ConfigService configService;
/**
* 文件检测,默认:不开启
*/
public boolean isFileDetectFlag() {
return fileDetectFlag;
}
/**
* 文件大小限制,单位 mb (默认50 mb)
*/
public long getMaxUploadFileSizeMb() {
return maxUploadFileSizeMb;
}
/**
* 连续登录失败次数则锁定,-1表示不受限制可以一直尝试登录
*/
public int getLoginFailMaxTimes() {
return loginFailMaxTimes;
}
/**
* 连续登录失败锁定时间(单位:秒),-1表示不锁定建议锁定30分钟
*/
public int getLoginFailLockSeconds() {
return loginFailLockSeconds;
}
/**
* 最低活跃时间(单位:秒),超过此时间没有操作系统就会被冻结,默认-1 代表不限制,永不冻结; 默认 30分钟
*/
public int getLoginActiveTimeoutSeconds() {
return loginActiveTimeoutSeconds;
}
/**
* 定期修改密码时间间隔默认默认建议90天更换密码
*/
public int getRegularChangePasswordDays() {
return regularChangePasswordDays;
}
/**
* 开启双因子登录,默认:开启
*/
public boolean isTwoFactorLoginEnabled() {
return twoFactorLoginEnabled;
}
/**
* 密码复杂度 是否开启,默认:开启
*/
public boolean isPasswordComplexityEnabled() {
return passwordComplexityEnabled;
}
/**
* 定期修改密码不允许相同次数默认3次以内密码不能相同
*/
public int getRegularChangePasswordNotAllowRepeatTimes() {
return regularChangePasswordNotAllowRepeatTimes;
}
@PostConstruct
void init() {
String configValue = configService.getConfigValue(ConfigKeyEnum.LEVEL3_PROTECT_CONFIG);
if (StrUtil.isEmpty(configValue)) {
throw new ExceptionInInitializerError("t_config 表 三级等保配置为空,请进行配置!");
}
Level3ProtectConfigForm level3ProtectConfigForm = JSON.parseObject(configValue, Level3ProtectConfigForm.class);
setProp(level3ProtectConfigForm);
}
/**
* 设置属性
*/
private void setProp(Level3ProtectConfigForm configForm) {
if (configForm.getFileDetectFlag() != null) {
this.fileDetectFlag = configForm.getFileDetectFlag();
}
if (configForm.getMaxUploadFileSizeMb() != null) {
this.maxUploadFileSizeMb = configForm.getMaxUploadFileSizeMb();
}
if (configForm.getLoginFailLockMinutes() != null) {
this.loginFailLockSeconds = configForm.getLoginFailLockMinutes() * 60;
}
if (configForm.getLoginActiveTimeoutMinutes() != null) {
this.loginActiveTimeoutSeconds = configForm.getLoginActiveTimeoutMinutes() * 60;
}
if (configForm.getPasswordComplexityEnabled() != null) {
this.passwordComplexityEnabled = configForm.getPasswordComplexityEnabled();
}
if (configForm.getRegularChangePasswordMonths() != null) {
this.regularChangePasswordDays = configForm.getRegularChangePasswordMonths() * 30;
}
if (configForm.getTwoFactorLoginEnabled() != null) {
this.twoFactorLoginEnabled = configForm.getTwoFactorLoginEnabled();
}
if (configForm.getRegularChangePasswordNotAllowRepeatTimes() != null) {
this.regularChangePasswordNotAllowRepeatTimes = configForm.getRegularChangePasswordNotAllowRepeatTimes();
}
// 设置 最低活跃时间(单位:秒)
if (this.loginActiveTimeoutSeconds > 0) {
StpUtil.getStpLogic().getConfigOrGlobal().setActiveTimeout(getLoginActiveTimeoutSeconds());
} else {
StpUtil.getStpLogic().getConfigOrGlobal().setActiveTimeout(-1);
}
}
/**
* 更新三级等保配置
*/
public ResponseDTO<String> updateLevel3Config(Level3ProtectConfigForm configForm) {
// 设置属性
setProp(configForm);
// 保存数据库
String configFormJsonString = JSON.toJSONString(configForm, true);
return configService.updateValueByKey(ConfigKeyEnum.LEVEL3_PROTECT_CONFIG, configFormJsonString);
}
}

View File

@@ -1,99 +0,0 @@
package net.lab1024.sa.base.module.support.securityprotect.service;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartStringUtil;
import net.lab1024.sa.base.module.support.apiencrypt.service.ApiEncryptService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 三级等保 密码 相关
*
* @Author 1024创新实验室-主任:卓大
* @Date 2023/10/11 19:25:59
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
*/
@Service
public class ProtectPasswordService {
/**
* 密码长度8-20位且包含大写字母、小写字母、数字三种
*/
public static final String PASSWORD_PATTERN = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,20}$";
/**
* 密码长度8-20位且包含大写字母、小写字母、数字三种
*/
public static final String PASSWORD_FORMAT_MSG = "密码必须为长度8-20位且包含大写字母、小写字母、数字三种";
private static final int PASSWORD_LENGTH = 8;
/**
* 密码复杂度开启, 默认为true 开启false 不开启
*/
@Value("${classified-protect.password-complexity-enabled}")
private Boolean passwordComplexityEnabled;
@Resource
private ApiEncryptService apiEncryptService;
/**
* 校验密码复杂度
*
* @return
*/
public ResponseDTO<String> validatePassComplexity(String password) {
// 无需校验
if (!passwordComplexityEnabled) {
return ResponseDTO.ok();
}
if (SmartStringUtil.isEmpty(password)) {
return ResponseDTO.userErrorParam(PASSWORD_FORMAT_MSG);
}
if (!password.matches(PASSWORD_PATTERN)) {
return ResponseDTO.userErrorParam(PASSWORD_FORMAT_MSG);
}
return ResponseDTO.ok();
}
/**
* 随机生成密码
*
* @return
*/
public String randomPassword() {
// 未开启密码复杂度则由8为数字构成
if (passwordComplexityEnabled) {
return RandomStringUtils.randomNumeric(PASSWORD_LENGTH);
} else {
// 3位大写字母2位数字3位小写字母
return RandomStringUtils.randomAlphabetic(3).toUpperCase() + RandomStringUtils.randomNumeric(2) + RandomStringUtils.randomAlphabetic(3).toLowerCase();
}
}
/**
* 解密 SM4 or AES 加密过的密码
*
* @param encryptedPassword
* @return
*/
public String decryptPassword(String encryptedPassword) {
return apiEncryptService.decrypt(encryptedPassword);
}
}

View File

@@ -0,0 +1,52 @@
package net.lab1024.sa.base.module.support.securityprotect.service;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
/**
* 三级等保 文件上传 相关
*
* @Author 1024创新实验室-主任:卓大
* @Date 2024/08/22 19:25:59
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
*/
@Service
public class SecurityFileService {
@Resource
private Level3ProtectConfigService level3ProtectConfigService;
/**
* 检测文件安全类型
*/
public ResponseDTO<String> checkFile(MultipartFile file) {
// 检验文件大小
if (level3ProtectConfigService.getMaxUploadFileSizeMb() > 0) {
long maxSize = level3ProtectConfigService.getMaxUploadFileSizeMb() * 1024 * 1024;
if (file.getSize() > maxSize) {
return ResponseDTO.userErrorParam("上传文件最大为:" + level3ProtectConfigService.getMaxUploadFileSizeMb() + " mb");
}
}
// 文件类型安全检测
if (!level3ProtectConfigService.isFileDetectFlag()) {
return ResponseDTO.ok();
}
// 检测文件类型
// .....
return ResponseDTO.ok();
}
}
;

View File

@@ -12,7 +12,6 @@ import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailEntity
import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailQueryForm;
import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailVO;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -26,27 +25,18 @@ import java.util.List;
* @Date 2023/10/11 19:25:59
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
*/
@Service
public class ProtectLoginService {
public class SecurityLoginService {
private static final String LOGIN_LOCK_MSG = "您已连续登录失败%s次账号锁定%s分钟解锁时间为%s请您耐心等待";
private static final String LOGIN_FAIL_MSG = "登录名或密码错误!连续登录失败%s次账号将锁定%s分钟您还可以再尝试%s次";
/**
* 连续登录失败次数则锁定-1表示不受限制可以一直登录
*/
@Value("${classified-protect.login-max-fail-times}")
private Integer loginMaxFailTimes;
/**
* 连续登录失败锁定时间单位-1表示不锁定
*/
@Value("${classified-protect.login-fail-locked-seconds}")
private Integer loginFailLockedSeconds;
@Resource
private Level3ProtectConfigService level3ProtectConfigService;
@Resource
private LoginFailDao loginFailDao;
@@ -61,8 +51,8 @@ public class ProtectLoginService {
*/
public ResponseDTO<LoginFailEntity> checkLogin(Long userId, UserTypeEnum userType) {
// 无需校验
if (loginMaxFailTimes < 1) {
// 若登录最大失败次数小于1无需校验
if (level3ProtectConfigService.getLoginFailMaxTimes() < 1) {
return ResponseDTO.ok();
}
@@ -72,19 +62,24 @@ public class ProtectLoginService {
return ResponseDTO.ok();
}
// 校验次数
if (loginFailEntity.getLoginFailCount() < loginMaxFailTimes) {
// 校验登录失败次数
if (loginFailEntity.getLoginFailCount() < level3ProtectConfigService.getLoginFailMaxTimes()) {
return ResponseDTO.ok(loginFailEntity);
}
// 校验是否锁定
if (loginFailEntity.getLoginLockBeginTime() == null) {
return ResponseDTO.ok(loginFailEntity);
}
// 校验锁定时长
if(loginFailEntity.getLoginLockBeginTime().plusSeconds(loginFailLockedSeconds).isBefore(LocalDateTime.now())){
if (loginFailEntity.getLoginLockBeginTime().plusSeconds(level3ProtectConfigService.getLoginFailLockSeconds()).isBefore(LocalDateTime.now())) {
// 过了锁定时间
return ResponseDTO.ok(loginFailEntity);
}
LocalDateTime unlockTime = loginFailEntity.getLoginLockBeginTime().plusSeconds(loginFailLockedSeconds);
return ResponseDTO.error(UserErrorCode.LOGIN_FAIL_LOCK, String.format(LOGIN_LOCK_MSG, loginFailEntity.getLoginFailCount(), loginFailLockedSeconds / 60, LocalDateTimeUtil.formatNormal(unlockTime)));
LocalDateTime unlockTime = loginFailEntity.getLoginLockBeginTime().plusSeconds(level3ProtectConfigService.getLoginFailLockSeconds());
return ResponseDTO.error(UserErrorCode.LOGIN_FAIL_LOCK, String.format(LOGIN_LOCK_MSG, loginFailEntity.getLoginFailCount(), level3ProtectConfigService.getLoginFailLockSeconds() / 60, LocalDateTimeUtil.formatNormal(unlockTime)));
}
/**
@@ -96,43 +91,40 @@ public class ProtectLoginService {
*/
public String recordLoginFail(Long userId, UserTypeEnum userType, String loginName, LoginFailEntity loginFailEntity) {
// 无需校验
if (loginMaxFailTimes < 1) {
// 若登录最大失败次数小于1无需记录
if (level3ProtectConfigService.getLoginFailMaxTimes() < 1) {
return null;
}
// 登录失败
int loginFailCount = loginFailEntity == null ? 1 : loginFailEntity.getLoginFailCount() + 1;
boolean lockFlag = loginFailCount >= level3ProtectConfigService.getLoginFailMaxTimes();
LocalDateTime lockBeginTime = lockFlag ? LocalDateTime.now() : null;
if (loginFailEntity == null) {
loginFailEntity = LoginFailEntity.builder()
.userId(userId)
.userType(userType.getValue())
.loginName(loginName)
.loginFailCount(1)
.lockFlag(false)
.loginLockBeginTime(null).build();
.loginFailCount(loginFailCount)
.lockFlag(lockFlag)
.loginLockBeginTime(lockBeginTime)
.build();
loginFailDao.insert(loginFailEntity);
} else {
// 如果是已经锁定状态则重新计算
if(loginFailEntity.getLockFlag()){
loginFailEntity.setLockFlag(false);
loginFailEntity.setLoginFailCount(1);
loginFailEntity.setLoginLockBeginTime(null);
}else{
loginFailEntity.setLoginLockBeginTime(LocalDateTime.now());
loginFailEntity.setLoginFailCount(loginFailEntity.getLoginFailCount() + 1);
loginFailEntity.setLockFlag(loginFailEntity.getLoginFailCount() >= loginMaxFailTimes);
}
loginFailEntity.setLoginLockBeginTime(lockBeginTime);
loginFailEntity.setLoginFailCount(loginFailCount);
loginFailEntity.setLockFlag(lockFlag);
loginFailEntity.setLoginName(loginName);
loginFailDao.updateById(loginFailEntity);
}
// 提示信息
if (loginFailEntity.getLoginFailCount() >= loginMaxFailTimes) {
LocalDateTime unlockTime = loginFailEntity.getLoginLockBeginTime().plusSeconds(loginFailLockedSeconds);
return String.format(LOGIN_LOCK_MSG, loginFailEntity.getLoginFailCount(), loginFailLockedSeconds / 60, LocalDateTimeUtil.formatNormal(unlockTime));
if (lockFlag) {
LocalDateTime unlockTime = loginFailEntity.getLoginLockBeginTime().plusSeconds(level3ProtectConfigService.getLoginFailLockSeconds());
return String.format(LOGIN_LOCK_MSG, loginFailEntity.getLoginFailCount(), level3ProtectConfigService.getLoginFailLockSeconds() / 60, LocalDateTimeUtil.formatNormal(unlockTime));
} else {
return String.format(LOGIN_FAIL_MSG, loginMaxFailTimes, loginFailLockedSeconds / 60, loginMaxFailTimes - loginFailEntity.getLoginFailCount());
return String.format(LOGIN_FAIL_MSG, level3ProtectConfigService.getLoginFailMaxTimes(), level3ProtectConfigService.getLoginFailLockSeconds() / 60, level3ProtectConfigService.getLoginFailMaxTimes() - loginFailEntity.getLoginFailCount());
}
}
@@ -143,8 +135,9 @@ public class ProtectLoginService {
* @param userType
*/
public void removeLoginFail(Long userId, UserTypeEnum userType) {
// 无需校验
if (loginMaxFailTimes < 1) {
// 若登录最大失败次数小于1无需校验
if (level3ProtectConfigService.getLoginFailMaxTimes() < 1) {
return;
}
@@ -160,8 +153,7 @@ public class ProtectLoginService {
public PageResult<LoginFailVO> queryPage(LoginFailQueryForm queryForm) {
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
List<LoginFailVO> list = loginFailDao.queryPage(page, queryForm);
PageResult<LoginFailVO> pageResult = SmartPageUtil.convert2PageResult(page, list);
return pageResult;
return SmartPageUtil.convert2PageResult(page, list);
}
/**

View File

@@ -0,0 +1,149 @@
package net.lab1024.sa.base.module.support.securityprotect.service;
import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartStringUtil;
import net.lab1024.sa.base.module.support.securityprotect.dao.PasswordLogDao;
import net.lab1024.sa.base.module.support.securityprotect.domain.PasswordLogEntity;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* 三级等保 密码 相关
*
* @Author 1024创新实验室-主任:卓大
* @Date 2023/10/11 19:25:59
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
*/
@Service
public class SecurityPasswordService {
/**
* 密码长度8-20位且包含大小写字母、数字、特殊符号三种及以上组合
*/
public static final String PASSWORD_PATTERN = "^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\\W_!@#$%^&*`~()-+=]+$)(?![0-9\\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\\W_!@#$%^&*`~()-+=]*$";
public static final String PASSWORD_FORMAT_MSG = "密码必须为长度8-20位且必须包含大小写字母、数字、特殊符号@#$%^&*()_+-=)等三种字符";
private static final int PASSWORD_LENGTH = 8;
private static final String PASSWORD_SALT_FORMAT = "smart_%s_admin_$^&*";
@Resource
private PasswordLogDao passwordLogDao;
@Resource
private Level3ProtectConfigService level3ProtectConfigService;
/**
* 校验密码复杂度
*/
public ResponseDTO<String> validatePasswordComplexity(String password) {
if (SmartStringUtil.isEmpty(password)) {
return ResponseDTO.userErrorParam(PASSWORD_FORMAT_MSG);
}
// 密码长度必须大于等于8位
if (password.length() < PASSWORD_LENGTH) {
return ResponseDTO.userErrorParam(PASSWORD_FORMAT_MSG);
}
// 无需校验 密码复杂度
if (!level3ProtectConfigService.isPasswordComplexityEnabled()) {
return ResponseDTO.ok();
}
if (!password.matches(PASSWORD_PATTERN)) {
return ResponseDTO.userErrorParam(PASSWORD_FORMAT_MSG);
}
return ResponseDTO.ok();
}
/**
* 校验密码重复次数
*/
public ResponseDTO<String> validatePasswordRepeatTimes(RequestUser requestUser, String newPassword) {
// 密码重复次数小于1 无需校验
if (level3ProtectConfigService.getRegularChangePasswordNotAllowRepeatTimes() < 1) {
return ResponseDTO.ok();
}
// 检查最近几次是否有重复密码
List<String> oldPasswords = passwordLogDao.selectOldPassword(requestUser.getUserType().getValue(), requestUser.getUserId(), level3ProtectConfigService.getRegularChangePasswordNotAllowRepeatTimes());
if (oldPasswords != null && oldPasswords.contains(getEncryptPwd(newPassword))) {
return ResponseDTO.userErrorParam(String.format("与前%s个历史密码重复请换个密码!", level3ProtectConfigService.getRegularChangePasswordNotAllowRepeatTimes()));
}
return ResponseDTO.ok();
}
/**
* 随机生成密码
*/
public String randomPassword() {
// 未开启密码复杂度则由8为数字构成
if (!level3ProtectConfigService.isPasswordComplexityEnabled()) {
return RandomStringUtils.randomNumeric(PASSWORD_LENGTH);
}
// 3位大写字母2位数字2位小写字母 + 1位特殊符号
return RandomStringUtils.randomAlphabetic(3).toUpperCase()
+ RandomStringUtils.randomNumeric(2)
+ RandomStringUtils.randomAlphabetic(2).toLowerCase()
+ (ThreadLocalRandom.current().nextBoolean() ? "#" : "@");
}
/**
* 保存修改密码
*/
public void saveUserChangePasswordLog(RequestUser requestUser, String newPassword, String oldPassword) {
PasswordLogEntity passwordLogEntity = new PasswordLogEntity();
passwordLogEntity.setNewPassword(newPassword);
passwordLogEntity.setOldPassword(oldPassword);
passwordLogEntity.setUserId(requestUser.getUserId());
passwordLogEntity.setUserType(requestUser.getUserType().getValue());
passwordLogDao.insert(passwordLogEntity);
}
/**
* 检查是否需要修改密码
*/
public boolean checkNeedChangePassword(Integer userType, Long userId) {
if (level3ProtectConfigService.getRegularChangePasswordDays() < 1) {
return false;
}
PasswordLogEntity passwordLogEntity = passwordLogDao.selectLastByUserTypeAndUserId(userType, userId);
if (passwordLogEntity == null) {
return false;
}
LocalDateTime nextUpdateTime = passwordLogEntity.getCreateTime().plusDays(level3ProtectConfigService.getRegularChangePasswordDays());
return nextUpdateTime.isBefore(LocalDateTime.now());
}
/**
* 获取 加密后 的密码
*/
public static String getEncryptPwd(String password) {
return DigestUtils.md5Hex(String.format(PASSWORD_SALT_FORMAT, password));
}
}

View File

@@ -1,9 +1,9 @@
spring:
# 数据库连接信息
datasource:
url: jdbc:p6spy:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
url: jdbc:p6spy:mysql://47.96.105.74:11024/smart_admin_v3_dev?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: Zhuoda1024lab
password: 11024Lab
initial-size: 2
min-idle: 2
max-active: 10
@@ -22,10 +22,10 @@ spring:
# redis 连接池配置信息
redis:
database: 1
host: 127.0.0.1
port: 6379
password:
database: 7
host: 47.96.105.74
port: 6666
password: ASDasd123
timeout: 10000ms
lettuce:
pool:
@@ -34,11 +34,22 @@ spring:
max-idle: 3
max-wait: 30000ms
# 上传文件大小配置
servlet:
multipart:
max-file-size: 30MB
max-request-size: 30MB
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: ROIMSIQCEXHTQFTA
properties:
mail:
smtp:
auth: true
ssl:
enable: true
socketFactory:
class: com.sun.mail.util.MailSSLSocketFactory
fallback: false
debug: false
# json序列化相关配置
jackson:
@@ -76,8 +87,8 @@ file:
region: oss-cn-hangzhou
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: 1024lab-smart-admin
access-key:
secret-key:
access-key: LTAI5tBAbehjXWyAqLhc58e1
secret-key: asX6ZWutaoTbQL3GxsFs24CmfAcYu3
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
private-url-expire-seconds: 3600
@@ -87,6 +98,7 @@ springdoc:
enabled: true # 开关
doc-expansion: none #关闭展开
tags-sorter: alpha
server-base-url:
api-docs:
enabled: true # 开关
knife4j:

View File

@@ -22,6 +22,12 @@
<if test="query.userName != null and query.userName != ''">
AND INSTR(operate_user_name,#{query.userName})
</if>
<if test="query.keywords != null and query.keywords != ''">
AND (INSTR(`module`,#{query.keywords}) OR INSTR(content,#{query.keywords}))
</if>
<if test="query.requestKeywords != null and query.requestKeywords != ''">
AND (INSTR(`url`,#{query.requestKeywords}) OR INSTR(`method`,#{query.requestKeywords}) OR INSTR(`param`,#{query.requestKeywords}))
</if>
<if test="query.successFlag != null">
AND success_flag = #{query.successFlag}
</if>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.lab1024.sa.base.module.support.securityprotect.dao.PasswordLogDao">
<select id="selectLastByUserTypeAndUserId"
resultType="net.lab1024.sa.base.module.support.securityprotect.domain.PasswordLogEntity">
select
*
from t_password_log
where
user_id = #{userId}
and user_type = #{userType}
order by id desc
limit 1
</select>
<select id="selectOldPassword" resultType="java.lang.String">
select
new_password
from t_password_log
where
user_id = #{userId}
and user_type = #{userType}
order by id desc
limit #{limit}
</select>
</mapper>

View File

@@ -1,9 +1,9 @@
spring:
# 数据库连接信息
datasource:
url: jdbc:p6spy:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
url: jdbc:p6spy:mysql://47.96.105.74:11024/smart_admin_v3_dev?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: Zhuoda1024lab
password: 11024Lab
initial-size: 2
min-idle: 2
max-active: 10
@@ -22,10 +22,10 @@ spring:
# redis 连接池配置信息
redis:
database: 1
host: 127.0.0.1
port: 6379
password:
database: 7
host: 47.96.105.74
port: 6666
password: ASDasd123
timeout: 10000ms
lettuce:
pool:
@@ -34,11 +34,22 @@ spring:
max-idle: 3
max-wait: 30000ms
# 上传文件大小配置
servlet:
multipart:
max-file-size: 30MB
max-request-size: 30MB
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: ROIMSIQCEXHTQFTA
properties:
mail:
smtp:
auth: true
ssl:
enable: true
socketFactory:
class: com.sun.mail.util.MailSSLSocketFactory
fallback: false
debug: false
# json序列化相关配置
jackson:
@@ -65,7 +76,6 @@ server:
max-days: 7
pattern: "%t %{X-Forwarded-For}i %a %r %s (%D ms) %I (%B byte)"
# 文件上传 配置
file:
storage:
@@ -77,24 +87,24 @@ file:
region: oss-cn-hangzhou
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: 1024lab-smart-admin
access-key:
secret-key:
access-key: LTAI5tBAbehjXWyAqLhc58e1
secret-key: asX6ZWutaoTbQL3GxsFs24CmfAcYu3
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
private-url-expire-seconds: 3600
# open api配置
springdoc:
swagger-ui:
enabled: true # 开关
doc-expansion: none #关闭展开
tags-sorter: alpha
server-base-url:
api-docs:
enabled: true # 开关
knife4j:
enable: true
basic:
enable: true
enable: false
username: api # Basic认证用户名
password: 1024 # Basic认证密码
@@ -112,11 +122,11 @@ access-control-allow-origin: '*'
# 心跳配置
heart-beat:
interval-seconds: 60
interval-seconds: 300
# 热加载配置
reload:
interval-seconds: 60
interval-seconds: 300
# sa-token 配置
sa-token:
@@ -135,9 +145,9 @@ sa-token:
# 是否打开自动续签 如果此值为true框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
auto-renew: true
# 是否输出操作日志
is-log: false
is-log: true
# 日志等级trace、debug、info、warn、error、fatal
log-level: warn
log-level: debug
# 启动时的字符画打印
is-print: false
# 是否从cookie读取token

View File

@@ -33,12 +33,22 @@ spring:
min-idle: 10
max-idle: 50
max-wait: 30000ms
# 上传文件大小配置
servlet:
multipart:
max-file-size: 30MB
max-request-size: 30MB
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1024lab
properties:
mail:
smtp:
auth: true
ssl:
enable: true
socketFactory:
class: com.sun.mail.util.MailSSLSocketFactory
fallback: false
debug: false
# json序列化相关配置
jackson:

View File

@@ -1,9 +1,9 @@
spring:
# 数据库连接信息
datasource:
url: jdbc:p6spy:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
url: jdbc:p6spy:mysql://47.96.105.74:11024/smart_admin_v3_dev?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: Zhuoda1024lab
password: 11024Lab
initial-size: 2
min-idle: 2
max-active: 10
@@ -22,10 +22,10 @@ spring:
# redis 连接池配置信息
redis:
database: 1
host: 127.0.0.1
port: 6379
password:
database: 7
host: 47.96.105.74
port: 6666
password: ASDasd123
timeout: 10000ms
lettuce:
pool:
@@ -34,11 +34,22 @@ spring:
max-idle: 3
max-wait: 30000ms
# 上传文件大小配置
servlet:
multipart:
max-file-size: 30MB
max-request-size: 30MB
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: ROIMSIQCEXHTQFTA
properties:
mail:
smtp:
auth: true
ssl:
enable: true
socketFactory:
class: com.sun.mail.util.MailSSLSocketFactory
fallback: false
debug: false
# json序列化相关配置
jackson:
@@ -65,7 +76,6 @@ server:
max-days: 7
pattern: "%t %{X-Forwarded-For}i %a %r %s (%D ms) %I (%B byte)"
# 文件上传 配置
file:
storage:
@@ -77,25 +87,24 @@ file:
region: oss-cn-hangzhou
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: 1024lab-smart-admin
access-key:
secret-key:
access-key: LTAI5tBAbehjXWyAqLhc58e1
secret-key: asX6ZWutaoTbQL3GxsFs24CmfAcYu3
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
private-url-expire-seconds: 3600
# open api配置
springdoc:
swagger-ui:
enabled: true # 开关
doc-expansion: none #关闭展开
tags-sorter: alpha
server-base-url: http://smartadmin.dev.1024lab.net/api/
server-base-url:
api-docs:
enabled: true # 开关
knife4j:
enable: true
basic:
enable: true
enable: false
username: api # Basic认证用户名
password: 1024 # Basic认证密码