1. 后台用户登录新增错误次数过多暂时封ip,次数和时间可以在yml里配置,防止恶意登录请求
2. FileUploadUtils里文件上传新增秒传功能,重复的文件不会重复上传了,文件的md5暂时存在redis里,后面可以存数据库,这样迁移更方便 3. 修复用户登录错误次数达上限后,禁止登录的时间不是yml配置的lockTime时间,而是默认的15天
This commit is contained in:
parent
ac9ef6ec1a
commit
276bb289bd
@ -46,7 +46,9 @@ user:
|
||||
maxRetryCount: 5
|
||||
# 密码锁定时间(默认10分钟)
|
||||
lockTime: 10
|
||||
|
||||
ip:
|
||||
maxRetryCount: 15
|
||||
lockTime: 15
|
||||
# Spring配置
|
||||
spring:
|
||||
cache:
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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暂时被限制登录.");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
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加密方法
|
||||
@ -16,6 +19,14 @@ import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,9 @@ public class SysLoginService
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private SysPasswordService passwordService;
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
*
|
||||
@ -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());
|
||||
}
|
||||
|
@ -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,6 +19,8 @@ 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;
|
||||
|
||||
/**
|
||||
* 登录密码方法
|
||||
*
|
||||
@ -25,12 +29,18 @@ import com.ruoyi.framework.security.context.AuthenticationContextHolder;
|
||||
@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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user