!3 修复登录bug,新增秒传和登录封ip功能

Merge pull request !3 from xx_fmy/xander
This commit is contained in:
Dftre 2024-08-22 02:46:19 +00:00 committed by Gitee
commit 896e88c8e1
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
9 changed files with 262 additions and 12 deletions

View File

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

View File

@ -41,6 +41,11 @@ public class CacheConstants {
*/ */
public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt"; 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 * 手机号验证码 phone codes
*/ */
@ -50,4 +55,14 @@ public class CacheConstants {
* 邮箱验证码 * 邮箱验证码
*/ */
public static final String EMAIL_CODES = "email_codes"; 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.exception.file.FileNameLengthLimitExceededException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.sign.Md5Utils;
import com.ruoyi.common.utils.uuid.UUID; import com.ruoyi.common.utils.uuid.UUID;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -96,7 +97,11 @@ public class DiskFileService implements FileService {
File file = new File(fileAbs); File file = new File(fileAbs);
// 路径为文件且不为空则进行删除 // 路径为文件且不为空则进行删除
if (file.isFile() && file.exists()) { if (file.isFile() && file.exists()) {
String md5 = Md5Utils.getMd5(file);
flag = file.delete(); flag = file.delete();
if(flag) {
FileOperateUtils.deleteFileAndMd5ByMd5(md5);
}
} }
return flag; return flag;
} }

View File

@ -1,6 +1,10 @@
package com.ruoyi.common.utils.file; package com.ruoyi.common.utils.file;
import com.ruoyi.common.config.RuoYiConfig; 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 com.ruoyi.common.utils.spring.SpringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -35,8 +39,15 @@ public class FileOperateUtils {
*/ */
public static final String upload(MultipartFile file) throws IOException { public static final String upload(MultipartFile file) throws IOException {
try { try {
String md5 = Md5Utils.getMd5(file);
String pathForMd5 = FileOperateUtils.getFilePathForMd5(md5);
if (StringUtils.isNotEmpty(pathForMd5)) {
return pathForMd5;
}
FileUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); 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) { } catch (Exception e) {
throw new IOException(e.getMessage(), e); throw new IOException(e.getMessage(), e);
} }
@ -52,8 +63,15 @@ public class FileOperateUtils {
* @throws IOException * @throws IOException
*/ */
public static final String upload(String filePath, MultipartFile file, String[] allowedExtension) throws Exception { 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); FileUtils.assertAllowed(file, allowedExtension);
return fileService.upload(filePath, file); String pathFileName = fileService.upload(filePath, file);
FileOperateUtils.saveFileAndMd5(pathFileName, md5);
return pathFileName;
} }
/** /**
@ -100,4 +118,46 @@ public class FileOperateUtils {
public static final boolean deleteFile(String fileUrl) throws Exception { public static final boolean deleteFile(String fileUrl) throws Exception {
return fileService.deleteFile(fileUrl); 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,12 +1,15 @@
package com.ruoyi.common.utils.sign; package com.ruoyi.common.utils.sign;
import java.io.UnsupportedEncodingException; import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
/** /**
* Md5加密方法 * Md5加密方法
@ -16,6 +19,14 @@ import org.slf4j.LoggerFactory;
public class Md5Utils { public class Md5Utils {
private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); 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) { private static byte[] md5(String s) {
MessageDigest algorithm; MessageDigest algorithm;
try { try {
@ -62,4 +73,91 @@ public class Md5Utils {
public static String encryptMd5(String string, String charSet) throws UnsupportedEncodingException { public static String encryptMd5(String string, String charSet) throws UnsupportedEncodingException {
return DigestUtils.md5Hex(string.getBytes(charSet)); 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

@ -51,6 +51,9 @@ public class SysLoginService
@Autowired @Autowired
private ISysConfigService configService; private ISysConfigService configService;
@Autowired
private SysPasswordService passwordService;
/** /**
* 登录验证 * 登录验证
* *
@ -66,6 +69,9 @@ public class SysLoginService
validateCaptcha(username, code, uuid); validateCaptcha(username, code, uuid);
// 登录前置校验 // 登录前置校验
loginPreCheck(username, password); loginPreCheck(username, password);
String ip = IpUtils.getIpAddr();
// 验证 IP 是否被封锁
passwordService.validateIp(ip);
// 用户验证 // 用户验证
Authentication authentication = null; Authentication authentication = null;
try try
@ -84,6 +90,7 @@ public class SysLoginService
} }
else else
{ {
passwordService.incrementIpFailCount(ip);
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage()); throw new ServiceException(e.getMessage());
} }

View File

@ -1,5 +1,7 @@
package com.ruoyi.framework.web.service; 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.beans.factory.annotation.Value;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -17,6 +19,8 @@ import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder; import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import java.util.concurrent.TimeUnit;
/** /**
* 登录密码方法 * 登录密码方法
* *
@ -25,12 +29,18 @@ import com.ruoyi.framework.security.context.AuthenticationContextHolder;
@Component @Component
public class SysPasswordService public class SysPasswordService
{ {
@Value(value = "${user.password.maxRetryCount}") @Value(value = "${user.password.maxRetryCount}")
private int maxRetryCount; private int maxRetryCount;
@Value(value = "${user.password.lockTime}") @Value(value = "${user.password.lockTime}")
private int 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); return CacheUtils.getCache(CacheConstants.PWD_ERR_CNT_KEY);
} }
private Cache getIpCache() {
return CacheUtils.getCache(CacheConstants.IP_ERR_CNT_KEY);
}
public void validate(SysUser user) public void validate(SysUser user)
{ {
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String username = usernamePasswordAuthenticationToken.getName(); String username = usernamePasswordAuthenticationToken.getName();
String password = usernamePasswordAuthenticationToken.getCredentials().toString(); String password = usernamePasswordAuthenticationToken.getCredentials().toString();
String ip = IpUtils.getIpAddr();
validateIp(ip);
Integer retryCount = getCache().get(username, Integer.class); Integer retryCount = getCache().get(username, Integer.class);
if (retryCount == null) if (retryCount == null)
{ {
@ -62,7 +79,7 @@ public class SysPasswordService
retryCount = retryCount + 1; retryCount = retryCount + 1;
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.retry.limit.count", retryCount))); 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(); throw new UserPasswordNotMatchException();
} }
else 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) public boolean matches(SysUser user, String rawPassword)
{ {
return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); 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.File;
import java.io.InputStream; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; 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.config.MinioConfig;
import com.ruoyi.middleware.minio.domain.MinioFileVO; import com.ruoyi.middleware.minio.domain.MinioFileVO;
import static com.ruoyi.common.utils.file.FileUtils.getPathFileName;
/** /**
* Minio文件操作实现类 * Minio文件操作实现类
*/ */
@ -32,6 +36,7 @@ public class MinioFileService implements FileService {
} else { } else {
relativePath = filePath; relativePath = filePath;
} }
return MinioUtil.uploadFile(minioConfig.getMasterClient().getDefaultBuket(), relativePath, file); return MinioUtil.uploadFile(minioConfig.getMasterClient().getDefaultBuket(), relativePath, file);
} }
@ -63,6 +68,7 @@ public class MinioFileService implements FileService {
public boolean deleteFile(String fileUrl) throws Exception { public boolean deleteFile(String fileUrl) throws Exception {
String filePath = StringUtils.substringAfter(fileUrl, "?fileName="); String filePath = StringUtils.substringAfter(fileUrl, "?fileName=");
MinioUtil.removeFile(minioConfig.getMasterClient().getDefaultBuket(), filePath); MinioUtil.removeFile(minioConfig.getMasterClient().getDefaultBuket(), filePath);
FileOperateUtils.deleteFileAndMd5ByFilePath(filePath);
return true; return true;
} }
} }