diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index a165e8f..855a882 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -46,7 +46,9 @@ user: maxRetryCount: 5 # 密码锁定时间(默认10分钟) lockTime: 10 - + ip: + maxRetryCount: 15 + lockTime: 15 # Spring配置 spring: cache: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java index dec0254..8b7d60a 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -2,7 +2,7 @@ package com.ruoyi.common.constant; /** * 缓存的key 常量 - * + * * @author ruoyi */ public class CacheConstants { @@ -41,6 +41,11 @@ public class CacheConstants { */ public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt"; + /** + * 登录ip错误次数 redis key + */ + public static final String IP_ERR_CNT_KEY = "ip_err_cnt_key"; + /** * 手机号验证码 phone codes */ @@ -50,4 +55,14 @@ public class CacheConstants { * 邮箱验证码 */ public static final String EMAIL_CODES = "email_codes"; + + /** + * 文件的md5 redis key + */ + public static final String FILE_MD5_PATH_KEY = "file_md5_path"; + + /** + * 文件路径 redis key + */ + public static final String FILE_PATH_MD5_KEY = "file_path_md5"; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/IpRetryLimitExceedException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/IpRetryLimitExceedException.java new file mode 100644 index 0000000..8eafd6d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/IpRetryLimitExceedException.java @@ -0,0 +1,15 @@ +package com.ruoyi.common.exception.user; + +/** + * IP 登录重试次数超限异常类 + * + */ +public class IpRetryLimitExceedException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public IpRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("失败次数过多,你的ip暂时被限制登录."); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/DiskFileService.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/DiskFileService.java index 51c47b5..f3564d3 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/DiskFileService.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/DiskFileService.java @@ -5,6 +5,7 @@ import com.ruoyi.common.constant.Constants; import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sign.Md5Utils; import com.ruoyi.common.utils.uuid.UUID; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -96,7 +97,11 @@ public class DiskFileService implements FileService { File file = new File(fileAbs); // 路径为文件且不为空则进行删除 if (file.isFile() && file.exists()) { + String md5 = Md5Utils.getMd5(file); flag = file.delete(); + if(flag) { + FileOperateUtils.deleteFileAndMd5ByMd5(md5); + } } return flag; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileOperateUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileOperateUtils.java index ffa06a1..50199e2 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileOperateUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileOperateUtils.java @@ -1,6 +1,10 @@ package com.ruoyi.common.utils.file; import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.utils.CacheUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sign.Md5Utils; import com.ruoyi.common.utils.spring.SpringUtils; import org.springframework.web.multipart.MultipartFile; @@ -35,8 +39,15 @@ public class FileOperateUtils { */ public static final String upload(MultipartFile file) throws IOException { try { + String md5 = Md5Utils.getMd5(file); + String pathForMd5 = FileOperateUtils.getFilePathForMd5(md5); + if (StringUtils.isNotEmpty(pathForMd5)) { + return pathForMd5; + } FileUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); - return fileService.upload(file); + String pathFileName = fileService.upload(file); + FileOperateUtils.saveFileAndMd5(pathFileName, md5); + return pathFileName; } catch (Exception e) { throw new IOException(e.getMessage(), e); } @@ -52,8 +63,15 @@ public class FileOperateUtils { * @throws IOException */ public static final String upload(String filePath, MultipartFile file, String[] allowedExtension) throws Exception { + String md5 = Md5Utils.getMd5(file); + String pathForMd5 = FileOperateUtils.getFilePathForMd5(md5); + if (StringUtils.isNotEmpty(pathForMd5)) { + return pathForMd5; + } FileUtils.assertAllowed(file, allowedExtension); - return fileService.upload(filePath, file); + String pathFileName = fileService.upload(filePath, file); + FileOperateUtils.saveFileAndMd5(pathFileName, md5); + return pathFileName; } /** @@ -67,7 +85,7 @@ public class FileOperateUtils { * @throws IOException */ public static final String upload(String baseDir, String fileName, MultipartFile file, - String[] allowedExtension) + String[] allowedExtension) throws IOException { try { String filePath = baseDir + File.separator + fileName; @@ -100,4 +118,46 @@ public class FileOperateUtils { public static final boolean deleteFile(String fileUrl) throws Exception { return fileService.deleteFile(fileUrl); } + + /** + * 根据md5获取文件的路径 + * + * @param md5 文件的md5 + * @return + */ + public static String getFilePathForMd5(String md5) { + return CacheUtils.get(CacheConstants.FILE_MD5_PATH_KEY, md5, String.class); + } + + /** + * 保存文件的md5 + * + * @param path 文件的路径 + * @param md5 文件的md5 + */ + public static void saveFileAndMd5(String path, String md5) { + CacheUtils.put(CacheConstants.FILE_MD5_PATH_KEY, md5, path); + CacheUtils.put(CacheConstants.FILE_PATH_MD5_KEY, path, md5); + } + + /** + * 删除文件的md5 + * + * @param md5 文件的md5 + */ + public static void deleteFileAndMd5ByMd5(String md5) { + String filePathByMd5 = getFilePathForMd5(md5); + if (StringUtils.isNotEmpty(filePathByMd5)) { + CacheUtils.remove(CacheConstants.FILE_MD5_PATH_KEY, md5); + CacheUtils.remove(CacheConstants.FILE_PATH_MD5_KEY, filePathByMd5); + } + } + + public static void deleteFileAndMd5ByFilePath(String filePath) { + String md5ByFilePath = CacheUtils.get(CacheConstants.FILE_PATH_MD5_KEY, filePath, String.class); + if (StringUtils.isNotEmpty(md5ByFilePath)) { + CacheUtils.remove(CacheConstants.FILE_PATH_MD5_KEY, filePath); + CacheUtils.remove(CacheConstants.FILE_MD5_PATH_KEY, md5ByFilePath); + } + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java index 43d770b..ac5d272 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java @@ -1,21 +1,32 @@ package com.ruoyi.common.utils.sign; -import java.io.UnsupportedEncodingException; +import java.io.*; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; /** * Md5加密方法 - * + * * @author ruoyi */ public class Md5Utils { private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + private static final ThreadLocal md5DigestThreadLocal = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found", e); + } + }); + private static byte[] md5(String s) { MessageDigest algorithm; try { @@ -62,4 +73,91 @@ public class Md5Utils { public static String encryptMd5(String string, String charSet) throws UnsupportedEncodingException { return DigestUtils.md5Hex(string.getBytes(charSet)); } + + /** + * 计算文件的md5 + * @param file 文件,可以是 MultipartFile 或 File + * @return + */ + public static String getMd5(Object file) { + try { + InputStream inputStream = getInputStream(file); + long fileSize = getFileSize(file); + // 10MB作为分界点 + if (fileSize < 10 * 1024 * 1024) { + return getMd5ForSmallFile(inputStream); + } else { + return getMd5ForLargeFile(inputStream); + } + } catch (Exception e) { + log.error(e.getMessage()); + } + return null; + } + + private static InputStream getInputStream(Object file) throws IOException { + if (file instanceof MultipartFile) { + return ((MultipartFile) file).getInputStream(); + } else if (file instanceof File) { + return new FileInputStream((File) file); + } + throw new IllegalArgumentException("Unsupported file type"); + } + + private static long getFileSize(Object file) throws IOException { + if (file instanceof MultipartFile) { + return ((MultipartFile) file).getSize(); + } else if (file instanceof File) { + return ((File) file).length(); + } + throw new IllegalArgumentException("Unsupported file type"); + } + + /** + * 计算小文件的md5 + * + * @param inputStream 文件输入流 + * @return + */ + private static String getMd5ForSmallFile(InputStream inputStream) { + try { + byte[] uploadBytes = inputStream.readAllBytes(); + MessageDigest md5 = md5DigestThreadLocal.get(); + byte[] digest = md5.digest(uploadBytes); + String md5Hex = new BigInteger(1, digest).toString(16); + while (md5Hex.length() < 32) { + md5Hex = "0" + md5Hex; + } + return md5Hex; + } catch (Exception e) { + log.error(e.getMessage()); + } + return null; + } + + /** + * 计算大文件的md5 + * + * @param inputStream 文件输入流 + * @return + */ + private static String getMd5ForLargeFile(InputStream inputStream) { + try (InputStream is = inputStream) { + MessageDigest md = md5DigestThreadLocal.get(); + byte[] buffer = new byte[81920]; + int read; + while ((read = is.read(buffer)) != -1) { + md.update(buffer, 0, read); + } + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + log.error(e.getMessage()); + } + return null; + } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index 350bed8..7de07fd 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -33,7 +33,7 @@ import jakarta.annotation.Resource; /** * 登录校验方法 - * + * * @author ruoyi */ @Component @@ -51,9 +51,12 @@ public class SysLoginService @Autowired private ISysConfigService configService; + @Autowired + private SysPasswordService passwordService; + /** * 登录验证 - * + * * @param username 用户名 * @param password 密码 * @param code 验证码 @@ -66,6 +69,9 @@ public class SysLoginService validateCaptcha(username, code, uuid); // 登录前置校验 loginPreCheck(username, password); + String ip = IpUtils.getIpAddr(); + // 验证 IP 是否被封锁 + passwordService.validateIp(ip); // 用户验证 Authentication authentication = null; try @@ -84,6 +90,7 @@ public class SysLoginService } else { + passwordService.incrementIpFailCount(ip); AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e.getMessage()); } @@ -101,7 +108,7 @@ public class SysLoginService /** * 校验验证码 - * + * * @param username 用户名 * @param code 验证码 * @param uuid 唯一标识 diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java index 9a0a833..cdb8316 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -1,5 +1,7 @@ package com.ruoyi.framework.web.service; +import com.ruoyi.common.exception.user.IpRetryLimitExceedException; +import com.ruoyi.common.utils.ip.IpUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.security.core.Authentication; @@ -17,20 +19,28 @@ import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import java.util.concurrent.TimeUnit; + /** * 登录密码方法 - * + * * @author ruoyi */ @Component public class SysPasswordService { + @Value(value = "${user.password.maxRetryCount}") private int maxRetryCount; @Value(value = "${user.password.lockTime}") private int lockTime; + @Value(value = "${user.ip.maxRetryCount:15}") + public int maxIpRetryCount; + + @Value(value = "${user.ip.lockTime:15}") + public int ipLockTime; /** * 登录账户密码错误次数缓存键名 * @@ -41,11 +51,18 @@ public class SysPasswordService return CacheUtils.getCache(CacheConstants.PWD_ERR_CNT_KEY); } + private Cache getIpCache() { + return CacheUtils.getCache(CacheConstants.IP_ERR_CNT_KEY); + } + public void validate(SysUser user) { Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); String username = usernamePasswordAuthenticationToken.getName(); String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + String ip = IpUtils.getIpAddr(); + validateIp(ip); Integer retryCount = getCache().get(username, Integer.class); if (retryCount == null) { @@ -62,7 +79,7 @@ public class SysPasswordService retryCount = retryCount + 1; AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.count", retryCount))); - getCache().put(username, retryCount); + CacheUtils.put(CacheConstants.PWD_ERR_CNT_KEY,username,retryCount,lockTime,TimeUnit.MINUTES); throw new UserPasswordNotMatchException(); } else @@ -71,6 +88,31 @@ public class SysPasswordService } } + public void validateIp(String ip) + { + Integer ipRetryCount = getIpCache().get(ip, Integer.class); + if (ipRetryCount == null) + { + ipRetryCount = 0; + } + + if (ipRetryCount >= maxIpRetryCount) + { + throw new IpRetryLimitExceedException(maxIpRetryCount, ipLockTime); + } + } + + public void incrementIpFailCount(String ip) + { + Integer ipRetryCount = getIpCache().get(ip, Integer.class); + if (ipRetryCount == null) + { + ipRetryCount = 0; + } + ipRetryCount += 1; + CacheUtils.put(CacheConstants.IP_ERR_CNT_KEY,ip,ipRetryCount,ipLockTime,TimeUnit.MINUTES); + } + public boolean matches(SysUser user, String rawPassword) { return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); diff --git a/ruoyi-middleware/ruoyi-middleware-minio/src/main/java/com/ruoyi/middleware/minio/utils/MinioFileService.java b/ruoyi-middleware/ruoyi-middleware-minio/src/main/java/com/ruoyi/middleware/minio/utils/MinioFileService.java index b49e11c..7057a27 100644 --- a/ruoyi-middleware/ruoyi-middleware-minio/src/main/java/com/ruoyi/middleware/minio/utils/MinioFileService.java +++ b/ruoyi-middleware/ruoyi-middleware-minio/src/main/java/com/ruoyi/middleware/minio/utils/MinioFileService.java @@ -3,6 +3,8 @@ package com.ruoyi.middleware.minio.utils; import java.io.File; import java.io.InputStream; +import com.ruoyi.common.utils.file.FileOperateUtils; +import com.ruoyi.common.utils.sign.Md5Utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @@ -15,6 +17,8 @@ import com.ruoyi.common.utils.file.FileUtils; import com.ruoyi.middleware.minio.config.MinioConfig; import com.ruoyi.middleware.minio.domain.MinioFileVO; +import static com.ruoyi.common.utils.file.FileUtils.getPathFileName; + /** * Minio文件操作实现类 */ @@ -32,6 +36,7 @@ public class MinioFileService implements FileService { } else { relativePath = filePath; } + return MinioUtil.uploadFile(minioConfig.getMasterClient().getDefaultBuket(), relativePath, file); } @@ -63,6 +68,7 @@ public class MinioFileService implements FileService { public boolean deleteFile(String fileUrl) throws Exception { String filePath = StringUtils.substringAfter(fileUrl, "?fileName="); MinioUtil.removeFile(minioConfig.getMasterClient().getDefaultBuket(), filePath); + FileOperateUtils.deleteFileAndMd5ByFilePath(filePath); return true; } }