1. 后台用户登录新增错误次数过多暂时封ip,次数和时间可以在yml里配置,防止恶意登录请求

2. FileUploadUtils里文件上传新增秒传功能,重复的文件不会重复上传了,文件的md5暂时存在redis里,后面可以存数据库,这样迁移更方便

3. 修复用户登录错误次数达上限后,禁止登录的时间不是yml配置的lockTime时间,而是默认的15天
This commit is contained in:
徐祥 2024-07-22 14:32:51 +08:00
parent ac9ef6ec1a
commit 276bb289bd
9 changed files with 262 additions and 12 deletions

View File

@ -46,7 +46,9 @@ user:
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
ip:
maxRetryCount: 15
lockTime: 15
# Spring配置
spring:
cache:

View File

@ -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";
}

View File

@ -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暂时被限制登录.");
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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<MessageDigest> 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;
}
}

View File

@ -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 唯一标识

View File

@ -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());

View File

@ -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;
}
}