东阳橙叶 發表於 2025-6-6 10:30:00

基于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">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
&lt;/dependency&gt;
</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 &amp; Setters
}
</code></pre>
</li>
<li>
<p>登录防护服务类</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class LoginSecurityService {
    private final RedisTemplate&lt;String, Object&gt; 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 &gt;= (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 &gt;= 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) &amp;&amp; " +
            "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) &amp;&amp; !"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&lt;String&gt; handleAccountLocked() {
      return ResponseEntity.status(423).body("Account temporarily locked");
    }

    @ExceptionHandler(IPRateLimitedException.class)
    public ResponseEntity&lt;String&gt; 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&lt;String, Object&gt; redisTemplate(RedisConnectionFactory factory) {
      RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();
      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]
查看完整版本: 基于Spring Boot 3 + AOP实现的完整登录防护方案代码,整合账号IP双维度防护和混合检测策略