时间:2025-06-06 10:30
人气:
作者:admin
以下是基于Spring Boot 3 + AOP实现的完整登录防护方案代码,整合账号/IP双维度防护和混合检测策略:
引入必要依赖(pom.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>
登录日志实体类
@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
}
登录防护服务类
@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");
}
}
AOP切面实现
@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[0];
String password = (String) args[1];
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)
}
}
工具类
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(",")[0];
}
ip = request.getHeader("Proxy-Client-IP");
return StringUtils.hasText(ip) ? ip : request.getRemoteAddr();
}
public static String normalize(String ip) {
return ip.contains(":") ? "[IPv6]" : ip;
}
}
异常处理
@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");
}
}
配置类(Redis和异步配置)
@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;
}
}
分层防护体系:
AOP实现优势:
性能优化:
防御增强:
在登录接口方法添加@PostMapping注解
配置Redis连接信息(application.properties):
spring.redis.host=localhost
spring.redis.port=6379
spring.data.redis.repositories.enabled=false
配置数据库连接(MySQL示例):
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/security_db
该方案可防御以下攻击向量:
建议配合WAF和系统防火墙构建纵深防御体系,并根据实际业务流量调整阈值参数。
本文来自博客园,作者:一块白板,转载请注明原文链接:https://www.cnblogs.com/ykbb/p/18913671