!58 修复:登录失败锁定不生效 、账号删除后依然能登录、用户名枚举漏洞

Merge pull request !58 from 憨涛子/master
This commit is contained in:
1024创新实验室
2025-03-11 07:26:43 +00:00
committed by Gitee
19 changed files with 509 additions and 269 deletions

View File

@@ -21,6 +21,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springboot.version>3.3.1</springboot.version>
<spring-mock.version>2.0.8</spring-mock.version>
<spring-security-crypto.version>6.4.3</spring-security-crypto.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<knife4j.version>4.4.0</knife4j.version>
@@ -48,7 +49,7 @@
<velocity-tools.version>3.1</velocity-tools.version>
<sa-token.version>1.37.0</sa-token.version>
<ip2region.version>2.7.0</ip2region.version>
<bcprov.version>1.59</bcprov.version>
<bcprov.version>1.80</bcprov.version>
<jackson-datatype-jsr310.version>2.13.4</jackson-datatype-jsr310.version>
<jackson-dataformat-yaml.version>2.16.1</jackson-dataformat-yaml.version>
<smartdb.version>1.2.0</smartdb.version>
@@ -56,6 +57,7 @@
<snakeyaml.version>2.2</snakeyaml.version>
<freemarker.version>2.3.33</freemarker.version>
<jsoup.version>1.18.1</jsoup.version>
<tika.version>3.1.0</tika.version>
</properties>
<dependencyManagement>
@@ -83,6 +85,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${spring-security-crypto.version}</version>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
@@ -235,7 +243,7 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bcprov.version}</version>
</dependency>
@@ -318,6 +326,12 @@
<version>${freemarker.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>${tika.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -407,4 +421,4 @@
</profile>
</profiles>
</project>
</project>

View File

@@ -3,6 +3,8 @@ package net.lab1024.sa.admin.module.system.department.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import lombok.Data;
import java.time.LocalDateTime;
@@ -34,6 +36,7 @@ public class DepartmentEntity {
/**
* 负责人员工 id
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private Long managerId;
/**

View File

@@ -297,38 +297,39 @@ public class EmployeeService {
if (employeeEntity == null) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
// 校验原始密码
String oldPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getOldPassword());
if (!Objects.equals(oldPassword, employeeEntity.getLoginPwd())) {
if (!SecurityPasswordService.matchesPwd(updatePasswordForm.getOldPassword(),employeeEntity.getLoginPwd()) ) {
return ResponseDTO.userErrorParam("原密码有误,请重新输入");
}
// 新旧密码相同
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword()) ){
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入");
}
// 校验密码复杂度
ResponseDTO<String> validatePassComplexity = securityPasswordService.validatePasswordComplexity(updatePasswordForm.getNewPassword());
if (!validatePassComplexity.getOk()) {
return validatePassComplexity;
}
// 新旧密码相同
String newPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword());
if (Objects.equals(oldPassword, newPassword)) {
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入");
}
// 根据三级等保规则,校验密码是否重复
ResponseDTO<String> passwordRepeatTimes = securityPasswordService.validatePasswordRepeatTimes(requestUser, updatePasswordForm.getNewPassword());
if (!passwordRepeatTimes.getOk()) {
return ResponseDTO.error(passwordRepeatTimes);
}
// 更新密码
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword());
EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId);
updateEntity.setLoginPwd(newPassword);
updateEntity.setLoginPwd(newEncryptPassword);
employeeDao.updateById(updateEntity);
// 保存修改密码密码记录
securityPasswordService.saveUserChangePasswordLog(requestUser, newPassword, oldPassword);
securityPasswordService.saveUserChangePasswordLog(requestUser, newEncryptPassword, employeeEntity.getLoginPwd());
return ResponseDTO.ok();
}

View File

@@ -162,10 +162,15 @@ public class LoginService implements StpInterface {
// 验证登录名
EmployeeEntity employeeEntity = employeeService.getByLoginName(loginForm.getLoginName());
if (null == employeeEntity) {
return ResponseDTO.userErrorParam("登录名不存在");
return ResponseDTO.userErrorParam("登录名或密码错误");
}
// 验证账号状态
if (employeeEntity.getDeletedFlag()) {
saveLoginLog(employeeEntity, ip, userAgent, "账号已删除", LoginLogResultEnum.LOGIN_FAIL);
return ResponseDTO.userErrorParam("您的账号已被删除,请联系工作人员!");
}
if (employeeEntity.getDisabledFlag()) {
saveLoginLog(employeeEntity, ip, userAgent, "账号已禁用", LoginLogResultEnum.LOGIN_FAIL);
return ResponseDTO.userErrorParam("您的账号已被禁用,请联系工作人员!");
@@ -201,7 +206,7 @@ public class LoginService implements StpInterface {
}
// 密码错误
if (!employeeEntity.getLoginPwd().equals(SecurityPasswordService.getEncryptPwd(requestPassword))) {
if ( !SecurityPasswordService.matchesPwd(requestPassword,employeeEntity.getLoginPwd()) ) {
// 记录登录失败
saveLoginLog(employeeEntity, ip, userAgent, "密码错误", LoginLogResultEnum.LOGIN_FAIL);
// 记录等级保护次数
@@ -504,10 +509,14 @@ public class LoginService implements StpInterface {
// 验证登录名
EmployeeEntity employeeEntity = employeeService.getByLoginName(loginName);
if (null == employeeEntity) {
return ResponseDTO.userErrorParam("登录名不存在!");
return ResponseDTO.ok();
}
// 验证账号状态
if (employeeEntity.getDeletedFlag()) {
return ResponseDTO.userErrorParam("您的账号已被删除,请联系工作人员!");
}
if (employeeEntity.getDisabledFlag()) {
return ResponseDTO.userErrorParam("您的账号已被禁用,请联系工作人员!");
}

View File

@@ -49,6 +49,11 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- sa-token start -->
<dependency>
<groupId>cn.dev33</groupId>
@@ -204,7 +209,7 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
@@ -268,7 +273,12 @@
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
</dependency>
</dependencies>
</project>
</project>

View File

@@ -161,6 +161,10 @@ public class Level3ProtectConfigService {
this.maxUploadFileSizeMb = configForm.getMaxUploadFileSizeMb();
}
if (configForm.getLoginFailMaxTimes() != null) {
this.loginFailMaxTimes = configForm.getLoginFailMaxTimes();
}
if (configForm.getLoginFailLockMinutes() != null) {
this.loginFailLockSeconds = configForm.getLoginFailLockMinutes() * 60;
}

View File

@@ -2,10 +2,19 @@ package net.lab1024.sa.base.module.support.securityprotect.service;
import jakarta.annotation.Resource;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.apache.tika.config.TikaConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.mime.MimeTypes;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 三级等保 文件上传 相关
@@ -23,6 +32,28 @@ public class SecurityFileService {
@Resource
private Level3ProtectConfigService level3ProtectConfigService;
// 定义白名单MIME类型
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList(
"application/json",
"application/zip",
"application/x-7z-compressed",
"application/pdf",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-works",
"text/csv",
"audio/*",
"video/*",
// 图片类型 svg有安全隐患所以不使用"image/*"
"image/jpeg",
"image/png",
"image/gif",
"image/bmp"
);
/**
* 检测文件安全类型
@@ -38,15 +69,50 @@ public class SecurityFileService {
}
// 文件类型安全检测
if (!level3ProtectConfigService.isFileDetectFlag()) {
return ResponseDTO.ok();
if (level3ProtectConfigService.isFileDetectFlag()) {
String fileType = getFileMimeType(file);
if(ALLOWED_MIME_TYPES.stream()
.noneMatch(allowedType -> matchesMimeType(fileType, allowedType))){
return ResponseDTO.userErrorParam("禁止上传此文件类型");
}
}
// 检测文件类型
// .....
return ResponseDTO.ok();
}
/**
* 获取文件的 MIME 类型
*
* @param file 要检查的文件
* @return 文件的 MIME 类型
*
*/
public static String getFileMimeType(MultipartFile file) {
try {
TikaConfig tika = new TikaConfig();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
TikaInputStream stream = TikaInputStream.get(file.getInputStream());
MediaType mimetype = tika.getDetector().detect(stream, metadata);
return mimetype.toString();
} catch (IOException | TikaException e) {
return MimeTypes.OCTET_STREAM;
}
}
/**
* 检查文件的 MIME 类型是否与指定的MIME 类型匹配(支持通配符)
*
* @param fileType 文件的 MIME 类型
* @param mimetype MIME 类型(支持通配符)
* @return 是否匹配
*/
private static boolean matchesMimeType(String fileType, String mimetype) {
if (mimetype.endsWith("/*")) {
String prefix = mimetype.substring(0, mimetype.length() - 1);
return fileType.startsWith(prefix);
} else {
return fileType.equalsIgnoreCase(mimetype);
}
}
}
;

View File

@@ -8,6 +8,7 @@ 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.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@@ -46,6 +47,8 @@ public class SecurityPasswordService {
@Resource
private Level3ProtectConfigService level3ProtectConfigService;
static Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
/**
* 校验密码复杂度
*/
@@ -84,8 +87,9 @@ public class SecurityPasswordService {
// 检查最近几次是否有重复密码
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()));
boolean isDuplicate = oldPasswords.stream().anyMatch(oldPassword -> encoder.matches(newPassword, oldPassword));
if (isDuplicate) {
return ResponseDTO.userErrorParam(String.format("与前%d个历史密码重复请换个密码!", level3ProtectConfigService.getRegularChangePasswordNotAllowRepeatTimes()));
}
return ResponseDTO.ok();
@@ -143,7 +147,14 @@ public class SecurityPasswordService {
* 获取 加密后 的密码
*/
public static String getEncryptPwd(String password) {
return DigestUtils.md5Hex(String.format(PASSWORD_SALT_FORMAT, password));
return encoder.encode(password);
}
/**
* 校验密码是否匹配
*/
public static Boolean matchesPwd( String password, String encodedPassword){
return encoder.matches( password, encodedPassword);
}
}