基于Spring Boot 3 + AOP实现的完整登录防护方案代码,整合账号IP双维度防护和混合检测策略
<h1 id="基于spring-boot-3--aop实现的完整登录防护方案代码整合账号ip双维度防护和混合检测策略">基于Spring Boot 3 + AOP实现的完整登录防护方案代码,整合账号IP双维度防护和混合检测策略</h1><p>以下是基于Spring Boot 3 + AOP实现的完整登录防护方案代码,整合账号/IP双维度防护和混合检测策略:</p>
<ol>
<li>
<p>引入必要依赖(pom.xml)</p>
<pre><code class="language-xml"><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</code></pre>
</li>
<li>
<p>登录日志实体类</p>
<pre><code class="language-java">@Entity
@Table(name = "sys_login_log")
@EntityListeners(AuditingEntityListener.class)
public class LoginLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String ipAddress;
private String userAgent;
private LocalDateTime loginTime;
private Boolean success;
private String failureReason;
// Getters & Setters
}
</code></pre>
</li>
<li>
<p>登录防护服务类</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class LoginSecurityService {
private final RedisTemplate<String, Object> redisTemplate;
private final LoginLogRepository loginLogRepository;
// 参数配置(建议通过@ConfigurationProperties注入)
private static final int MAX_ACCOUNT_ATTEMPTS = 5;
private static final int MAX_IP_ATTEMPTS = 100;
private static final int TIME_WINDOW = 5; // 分钟
private static final int LOCK_TIME = 15; // 分钟
public boolean validateLogin(String username, String ip) {
// 账号维度检测
if (isAccountLocked(username)) {
recordLoginLog(username, ip, false, "Account locked");
throw new AccountLockedException();
}
// IP维度检测
if (isIPRateLimited(ip)) {
recordLoginLog(username, ip, false, "IP rate limited");
throw new IPRateLimitedException();
}
// 混合维度检测(IP下账号切换检测)
if (isSuspiciousSwitch(ip, username)) {
triggerMFA(username, ip);
return false;
}
return true;
}
public void recordFailure(String identifier, LoginType type) {
String key = buildKey(identifier, type);
int attempts = incrementWithExpire(key, type == LoginType.ACCOUNT ? MAX_ACCOUNT_ATTEMPTS : MAX_IP_ATTEMPTS, TIME_WINDOW);
if (attempts >= (type == LoginType.ACCOUNT ? MAX_ACCOUNT_ATTEMPTS : MAX_IP_ATTEMPTS)) {
lockResource(identifier, type, LOCK_TIME);
}
}
private String buildKey(String identifier, LoginType type) {
return String.format("login:%s:%s",
type == LoginType.ACCOUNT ? "account" : "ip",
type == LoginType.ACCOUNT ? identifier : IPUtils.normalize(identifier));
}
private boolean isAccountLocked(String username) {
return isLocked(username, LoginType.ACCOUNT);
}
private boolean isIPRateLimited(String ip) {
return isLocked(ip, LoginType.IP);
}
private boolean isLocked(String identifier, LoginType type) {
String lockKey = buildKey(identifier, type) + ":locked";
return Boolean.TRUE.equals(redisTemplate.hasKey(lockKey));
}
private void lockResource(String identifier, LoginType type, int minutes) {
String lockKey = buildKey(identifier, type) + ":locked";
redisTemplate.opsForValue().set(lockKey, "1", minutes, TimeUnit.MINUTES);
}
private int incrementWithExpire(String key, int threshold, int windowMinutes) {
Long count = redisTemplate.opsForValue().increment(key);
if (count == null) {
redisTemplate.expire(key, windowMinutes, TimeUnit.MINUTES);
return 0;
}
return count.intValue();
}
// 混合检测逻辑
private boolean isSuspiciousSwitch(String ip, String username) {
String switchKey = "login:switch:" + ip;
int switchCount = redisTemplate.opsForZSet().zCard(switchKey).intValue();
if (switchCount >= 3) {
return true;
}
redisTemplate.opsForZSet().add(switchKey, username, System.currentTimeMillis());
redisTemplate.expire(switchKey, 5, TimeUnit.MINUTES);
return false;
}
// 异步日志记录
@Async
public void recordLoginLog(String username, String ip, boolean success, String reason) {
LoginLog log = new LoginLog();
log.setUsername(username);
log.setIpAddress(ip);
log.setUserAgent(ServletUtils.getUserAgent());
log.setLoginTime(LocalDateTime.now());
log.setSuccess(success);
log.setFailureReason(reason);
loginLogRepository.save(log);
}
// 触发多因素认证
private void triggerMFA(String username, String ip) {
// 实现短信/邮件验证逻辑
recordLoginLog(username, ip, false, "MFA triggered");
}
}
</code></pre>
</li>
<li>
<p>AOP切面实现</p>
<pre><code class="language-java">@Aspect
@Component
@RequiredArgsConstructor
public class LoginSecurityAspect {
private final LoginSecurityService securityService;
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping) && " +
"execution(* com.example.controller.AuthController.login(..))")
public void loginEndpoint() {}
@Around("loginEndpoint()")
public Object validateLogin(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String username = (String) args;
String password = (String) args;
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String ip = IPUtils.getClientIp(request);
try {
// 执行防护验证
securityService.validateLogin(username, ip);
// 继续执行登录逻辑
Object result = joinPoint.proceed();
// 登录成功后重置计数器
securityService.resetAttempts(username, LoginType.ACCOUNT);
securityService.resetAttempts(ip, LoginType.IP);
return result;
} catch (AuthenticationException e) {
// 记录失败日志
securityService.recordFailure(username, LoginType.ACCOUNT);
securityService.recordFailure(ip, LoginType.IP);
securityService.recordLoginLog(username, ip, false, e.getMessage());
throw e;
}
}
@AfterThrowing(pointcut = "loginEndpoint()", throwing = "ex")
public void handleLoginFailure(Exception ex) {
// 统一异常处理(可结合@ControllerAdvice)
}
}
</code></pre>
</li>
<li>
<p>工具类</p>
<pre><code class="language-java">public class IPUtils {
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",");
}
ip = request.getHeader("Proxy-Client-IP");
return StringUtils.hasText(ip) ? ip : request.getRemoteAddr();
}
public static String normalize(String ip) {
return ip.contains(":") ? "" : ip;
}
}
</code></pre>
</li>
<li>
<p>异常处理</p>
<pre><code class="language-java">@ControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccountLockedException.class)
public ResponseEntity<String> handleAccountLocked() {
return ResponseEntity.status(423).body("Account temporarily locked");
}
@ExceptionHandler(IPRateLimitedException.class)
public ResponseEntity<String> handleIPRateLimit() {
return ResponseEntity.status(429).body("Too many requests, please try again later");
}
}
</code></pre>
</li>
<li>
<p>配置类(Redis和异步配置)</p>
<pre><code class="language-java">@Configuration
@EnableAsync
@EnableCaching
public class SecurityConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
}
</code></pre>
</li>
</ol>
<h3 id="方案特点">方案特点:</h3>
<ol>
<li>
<p><strong>分层防护体系</strong>:</p>
<ul>
<li>账号层:5次失败锁定15分钟</li>
<li>IP层:5分钟内100次失败触发限流</li>
<li>混合层:检测IP下账号切换行为,触发MFA</li>
</ul>
</li>
<li>
<p><strong>AOP实现优势</strong>:</p>
<ul>
<li>完全解耦安全逻辑与业务代码</li>
<li>集中管理横切关注点</li>
<li>支持动态扩展验证规则</li>
</ul>
</li>
<li>
<p><strong>性能优化</strong>:</p>
<ul>
<li>Redis原子计数器保证并发安全</li>
<li>异步日志写入避免阻塞主流程</li>
<li>本地缓存+Redis双缓冲机制(示例中未完全展示,可自行扩展)</li>
</ul>
</li>
<li>
<p><strong>防御增强</strong>:</p>
<ul>
<li>IPv6地址规范化处理</li>
<li>代理穿透式IP获取</li>
<li>滑动窗口计数算法(需自行扩展ZSET实现)</li>
</ul>
</li>
</ol>
<h3 id="使用说明">使用说明:</h3>
<ol>
<li>
<p>在登录接口方法添加<code>@PostMapping</code>注解</p>
</li>
<li>
<p>配置Redis连接信息(application.properties):</p>
<pre><code class="language-properties">spring.redis.host=localhost
spring.redis.port=6379
spring.data.redis.repositories.enabled=false
</code></pre>
</li>
<li>
<p>配置数据库连接(MySQL示例):</p>
<pre><code class="language-properties">spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/security_db
</code></pre>
</li>
</ol>
<p>该方案可防御以下攻击向量:</p>
<ul>
<li>单账号暴力破解</li>
<li>分布式IP扫描攻击</li>
<li>账号枚举攻击</li>
<li>慢速字典攻击</li>
</ul>
<p>建议配合WAF和系统防火墙构建纵深防御体系,并根据实际业务流量调整阈值参数。</p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:一块白板,转载请注明原文链接:https://www.cnblogs.com/ykbb/p/18913671</p><br><br>
来源:https://www.cnblogs.com/ykbb/p/18913671
頁:
[1]