fix(security): 修复重放攻击拦截器的时间戳验证漏洞 refactor(security): 重构验证码工具类使用线程安全实现 perf(login): 优化登录锁定工具类性能并添加定期清理 fix(editor): 修复笔记编辑器空指针问题 style: 清理数据库索引脚本中的冗余注释 fix(api): 修复前端API调用参数编码问题 feat(image): 实现图片名称同步服务 refactor(markdown): 重构Markdown服务分离图片名称同步逻辑 fix(xss): 添加HTML转义函数防止XSS攻击 fix(user): 修复用户服务权限加载问题 fix(rate-limit): 修复速率限制拦截器并发问题 fix(axios): 生产环境隐藏详细错误信息 fix(image): 修复图片上传和删除的权限验证 refactor(captcha): 重构验证码工具类使用并发安全实现 fix(jwt): 修复JWT过滤器空指针问题 fix(export): 修复笔记导出XSS漏洞 fix(search): 修复Markdown搜索SQL注入问题 fix(interceptor): 修复重放攻击拦截器逻辑错误 fix(controller): 修复用户控制器空指针问题 fix(security): 修复nonce生成使用密码学安全方法
217 lines
6.6 KiB
Java
217 lines
6.6 KiB
Java
package com.test.bijihoudaun.util;
|
||
|
||
import lombok.extern.slf4j.Slf4j;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.time.temporal.ChronoUnit;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.Map;
|
||
import java.util.concurrent.Executors;
|
||
import java.util.concurrent.ScheduledExecutorService;
|
||
import java.util.concurrent.TimeUnit;
|
||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||
|
||
/**
|
||
* 登录锁定工具类
|
||
* 使用本地内存存储登录失败记录,带容量限制和定期清理
|
||
*/
|
||
@Slf4j
|
||
public class LoginLockUtil {
|
||
|
||
// 最大失败次数
|
||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
||
// 锁定时间(分钟)
|
||
private static final int LOCK_TIME_MINUTES = 30;
|
||
// 失败记录过期时间(分钟)
|
||
private static final int RECORD_EXPIRE_MINUTES = 60;
|
||
// 最大存储记录数(防止内存溢出)
|
||
private static final int MAX_RECORDS = 10000;
|
||
|
||
// 登录失败记录:key=用户名,value=失败记录
|
||
private static final LRUCache<String, LoginAttempt> attempts = new LRUCache<>(MAX_RECORDS);
|
||
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||
|
||
// 定期清理线程池
|
||
private static final ScheduledExecutorService cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||
Thread t = new Thread(r, "login-lock-cleanup");
|
||
t.setDaemon(true);
|
||
return t;
|
||
});
|
||
|
||
static {
|
||
// 每10分钟清理一次过期记录
|
||
cleanupScheduler.scheduleAtFixedRate(LoginLockUtil::cleanupExpiredLocks,
|
||
10, 10, TimeUnit.MINUTES);
|
||
}
|
||
|
||
private static class LoginAttempt {
|
||
int failedCount;
|
||
LocalDateTime lastAttemptTime;
|
||
LocalDateTime lockUntil;
|
||
|
||
LoginAttempt() {
|
||
this.failedCount = 0;
|
||
this.lastAttemptTime = LocalDateTime.now();
|
||
this.lockUntil = null;
|
||
}
|
||
|
||
boolean isExpired() {
|
||
return ChronoUnit.MINUTES.between(lastAttemptTime, LocalDateTime.now()) > RECORD_EXPIRE_MINUTES;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 简单的 LRU 缓存实现
|
||
*/
|
||
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||
private final int maxSize;
|
||
|
||
LRUCache(int maxSize) {
|
||
super(maxSize, 0.75f, true);
|
||
this.maxSize = maxSize;
|
||
}
|
||
|
||
@Override
|
||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||
boolean shouldRemove = size() > maxSize;
|
||
if (shouldRemove) {
|
||
log.warn("登录锁定记录达到上限,移除最旧的记录: {}", eldest.getKey());
|
||
}
|
||
return shouldRemove;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录登录失败
|
||
* @param username 用户名
|
||
*/
|
||
public static void recordFailedAttempt(String username) {
|
||
if (username == null || username.isEmpty()) return;
|
||
|
||
lock.writeLock().lock();
|
||
try {
|
||
LoginAttempt attempt = attempts.computeIfAbsent(username, k -> new LoginAttempt());
|
||
attempt.failedCount++;
|
||
attempt.lastAttemptTime = LocalDateTime.now();
|
||
|
||
// 达到最大失败次数,锁定账号
|
||
if (attempt.failedCount >= MAX_FAILED_ATTEMPTS) {
|
||
attempt.lockUntil = LocalDateTime.now().plusMinutes(LOCK_TIME_MINUTES);
|
||
log.warn("账号 [{}] 已被锁定 {} 分钟", username, LOCK_TIME_MINUTES);
|
||
}
|
||
} finally {
|
||
lock.writeLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录登录成功(清除失败记录)
|
||
* @param username 用户名
|
||
*/
|
||
public static void recordSuccess(String username) {
|
||
if (username == null || username.isEmpty()) return;
|
||
|
||
lock.writeLock().lock();
|
||
try {
|
||
attempts.remove(username);
|
||
} finally {
|
||
lock.writeLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查账号是否被锁定
|
||
* @param username 用户名
|
||
* @return true-已锁定,false-未锁定
|
||
*/
|
||
public static boolean isLocked(String username) {
|
||
if (username == null || username.isEmpty()) return false;
|
||
|
||
lock.readLock().lock();
|
||
try {
|
||
LoginAttempt attempt = attempts.get(username);
|
||
if (attempt == null) return false;
|
||
|
||
// 检查是否仍在锁定时间内
|
||
if (attempt.lockUntil != null) {
|
||
return LocalDateTime.now().isBefore(attempt.lockUntil);
|
||
}
|
||
return false;
|
||
} finally {
|
||
lock.readLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取剩余锁定时间(秒)
|
||
* @param username 用户名
|
||
* @return 剩余秒数,如果未锁定返回0
|
||
*/
|
||
public static long getRemainingLockTime(String username) {
|
||
if (username == null || username.isEmpty()) return 0;
|
||
|
||
lock.readLock().lock();
|
||
try {
|
||
LoginAttempt attempt = attempts.get(username);
|
||
if (attempt == null || attempt.lockUntil == null) return 0;
|
||
|
||
long remaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), attempt.lockUntil);
|
||
return Math.max(0, remaining);
|
||
} finally {
|
||
lock.readLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取剩余失败次数
|
||
* @param username 用户名
|
||
* @return 剩余次数
|
||
*/
|
||
public static int getRemainingAttempts(String username) {
|
||
if (username == null || username.isEmpty()) return MAX_FAILED_ATTEMPTS;
|
||
|
||
lock.readLock().lock();
|
||
try {
|
||
LoginAttempt attempt = attempts.get(username);
|
||
if (attempt == null) return MAX_FAILED_ATTEMPTS;
|
||
|
||
return Math.max(0, MAX_FAILED_ATTEMPTS - attempt.failedCount);
|
||
} finally {
|
||
lock.readLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过期的锁定记录
|
||
*/
|
||
private static void cleanupExpiredLocks() {
|
||
lock.writeLock().lock();
|
||
try {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
attempts.entrySet().removeIf(entry -> {
|
||
LoginAttempt attempt = entry.getValue();
|
||
// 未锁定且长时间没有登录的记录
|
||
if (attempt.lockUntil == null) {
|
||
return attempt.isExpired();
|
||
}
|
||
// 锁定已过期的记录
|
||
return now.isAfter(attempt.lockUntil);
|
||
});
|
||
} finally {
|
||
lock.writeLock().unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前记录数量(用于监控)
|
||
*/
|
||
public static int getRecordCount() {
|
||
lock.readLock().lock();
|
||
try {
|
||
return attempts.size();
|
||
} finally {
|
||
lock.readLock().unlock();
|
||
}
|
||
}
|
||
}
|