Files
biji/biji-houdaun/src/main/java/com/test/bijihoudaun/util/LoginLockUtil.java
ikmkj 375ccb89ff feat: 添加用户角色字段并实现权限控制
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生成使用密码学安全方法
2026-03-03 20:48:40 +08:00

217 lines
6.6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}
}