← Назад к вопросам

Как предотвратить спам в приложении

1.8 Middle🔥 151 комментариев
#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Предотвращение спама в приложении

Спам — это один из главных вызовов для безопасности и пользовательского опыта веб-приложений. Существует множество техник для его предотвращения, от простых до сложных.

1. Rate Limiting (Ограничение частоты запросов)

1.1 Redis-based Rate Limiter

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class RateLimiter {
    
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    
    private static final int MAX_REQUESTS = 10;  // 10 запросов
    private static final long WINDOW_SIZE = 60;  // в течение 60 секунд
    
    public boolean isAllowed(String userId) {
        String key = "rate_limit:" + userId;
        Integer count = redisTemplate.opsForValue().get(key);
        
        if (count == null) {
            // первый запрос
            redisTemplate.opsForValue().set(key, 1, WINDOW_SIZE, TimeUnit.SECONDS);
            return true;
        }
        
        if (count < MAX_REQUESTS) {
            // запрос в пределах лимита
            redisTemplate.opsForValue().increment(key);
            return true;
        }
        
        // превышен лимит
        return false;
    }
}

// Использование в контроллере
@RestController
@RequestMapping("/api/v1/messages")
public class MessageController {
    
    @Autowired
    private RateLimiter rateLimiter;
    
    @PostMapping
    public ResponseEntity<?> sendMessage(@RequestBody MessageRequest request) {
        String userId = getCurrentUserId();
        
        if (!rateLimiter.isAllowed(userId)) {
            return ResponseEntity.status(429)  // Too Many Requests
                .body(new ErrorResponse("Rate limit exceeded. Try again later."));
        }
        
        // обработать сообщение
        return ResponseEntity.ok(new MessageResponse("Message sent"));
    }
}

1.2 Token Bucket Algorithm

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;

public class TokenBucketRateLimiter {
    
    private final Cache<String, com.google.common.util.concurrent.RateLimiter> limiters;
    private static final double PERMITS_PER_SECOND = 5.0;  // 5 запросов в секунду
    
    public TokenBucketRateLimiter() {
        this.limiters = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build();
    }
    
    public boolean acquire(String userId) {
        com.google.common.util.concurrent.RateLimiter limiter = limiters.asMap()
            .computeIfAbsent(userId, k -> 
                com.google.common.util.concurrent.RateLimiter.create(PERMITS_PER_SECOND));
        
        return limiter.tryAcquire();
    }
}

1.3 Аннотация для Rate Limiting

import org.springframework.web.bind.annotation.*;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
    int requests() default 10;
    int seconds() default 60;
}

@Aspect
@Component
public class RateLimitAspect {
    
    @Autowired
    private RateLimiter rateLimiter;
    
    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String userId = getCurrentUserId();
        
        if (!rateLimiter.isAllowed(userId)) {
            throw new RateLimitExceededException(
                String.format("Rate limit exceeded: %d requests per %d seconds",
                    rateLimit.requests(), rateLimit.seconds()));
        }
        
        return joinPoint.proceed();
    }
}

// Использование
@PostMapping("/send-email")
@RateLimit(requests = 5, seconds = 60)  // максимум 5 писем в минуту
public ResponseEntity<?> sendEmail(@RequestBody EmailRequest request) {
    // отправить письмо
    return ResponseEntity.ok("Email sent");
}

2. CAPTCHA (Challenge–Response Test)

2.1 Google reCAPTCHA v3

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.client.RestTemplate;
import org.springframework.stereotype.Service;

@Service
public class RecaptchaService {
    
    @Value("${recaptcha.secret.key}")
    private String secretKey;
    
    @Value("${recaptcha.api.url}")
    private String recaptchaApiUrl;
    
    private final RestTemplate restTemplate = new RestTemplate();
    
    public boolean verifyToken(String token) {
        try {
            RecaptchaVerificationRequest request = new RecaptchaVerificationRequest();
            request.setSecret(secretKey);
            request.setResponse(token);
            
            RecaptchaVerificationResponse response = restTemplate.postForObject(
                recaptchaApiUrl,
                request,
                RecaptchaVerificationResponse.class
            );
            
            // reCAPTCHA v3 возвращает score (0.0-1.0)
            // 1.0 — точно человек, 0.0 — точно бот
            return response != null && 
                   response.isSuccess() && 
                   response.getScore() > 0.5;  // пороговое значение
        } catch (Exception e) {
            throw new RuntimeException("CAPTCHA verification failed", e);
        }
    }
}

record RecaptchaVerificationRequest(
    String secret,
    String response
) {}

record RecaptchaVerificationResponse(
    boolean success,
    double score,
    String action,
    String challengeTs
) {}

// Использование в контроллере
@PostMapping("/register")
public ResponseEntity<?> register(
    @RequestBody RegisterRequest request,
    @RequestParam String recaptchaToken) {
    
    if (!recaptchaService.verifyToken(recaptchaToken)) {
        return ResponseEntity.status(400)
            .body(new ErrorResponse("CAPTCHA verification failed"));
    }
    
    // регистрация пользователя
    return ResponseEntity.ok(new RegisterResponse("Account created"));
}

3. Honeypot (Ловушка для ботов)

<!-- HTML форма с honeypot полем -->
<form method="post" action="/api/v1/contact">
    <input type="text" name="name" required>
    <input type="email" name="email" required>
    <textarea name="message" required></textarea>
    
    <!-- Honeypot: скрытое поле, которое должно быть пусто -->
    <input type="text" name="website" style="display: none;" tabindex="-1" autocomplete="off">
    
    <button type="submit">Send</button>
</form>
@PostMapping("/contact")
public ResponseEntity<?> submitContact(@RequestBody ContactForm form) {
    // Если honeypot поле заполнено, это бот
    if (form.getWebsite() != null && !form.getWebsite().isEmpty()) {
        // Молча игнорируем (или логируем как подозрительную активность)
        return ResponseEntity.ok(new ContactResponse("Thank you"));
    }
    
    // обработать форму
    return ResponseEntity.ok(new ContactResponse("Message received"));
}

4. Email Verification

@Service
public class EmailVerificationService {
    
    @Autowired
    private VerificationTokenRepository tokenRepository;
    
    @Autowired
    private EmailService emailService;
    
    public void sendVerificationEmail(String email) {
        String token = generateSecureToken();
        
        VerificationToken verToken = new VerificationToken();
        verToken.setEmail(email);
        verToken.setToken(token);
        verToken.setExpiryDate(LocalDateTime.now().plusHours(24));
        verToken.setVerified(false);
        
        tokenRepository.save(verToken);
        
        String verificationLink = "https://example.com/verify?token=" + token;
        emailService.sendVerificationEmail(email, verificationLink);
    }
    
    public boolean verifyEmail(String token) {
        VerificationToken verToken = tokenRepository.findByToken(token);
        
        if (verToken == null || verToken.isExpired()) {
            return false;
        }
        
        verToken.setVerified(true);
        tokenRepository.save(verToken);
        return true;
    }
}

@Entity
@Table(name = "verification_tokens")
public class VerificationToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String token;
    
    @Column(nullable = false)
    private String email;
    
    @Column(nullable = false)
    private LocalDateTime expiryDate;
    
    @Column(nullable = false)
    private boolean verified = false;
    
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expiryDate);
    }
}

5. IP Blocking и Blacklist

@Component
public class IpBlockingFilter extends OncePerRequestFilter {
    
    @Autowired
    private IpBlacklist ipBlacklist;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        String clientIp = getClientIp(request);
        
        if (ipBlacklist.isBlocked(clientIp)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write("Access denied: Your IP is blocked");
            return;
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getClientIp(HttpServletRequest request) {
        // Проверяем X-Forwarded-For для прокси
        String forwardedFor = request.getHeader("X-Forwarded-For");
        if (forwardedFor != null && !forwardedFor.isEmpty()) {
            return forwardedFor.split(",")[0].trim();
        }
        
        return request.getRemoteAddr();
    }
}

@Service
public class IpBlacklist {
    
    @Autowired
    private RedisTemplate<String, Boolean> redisTemplate;
    
    public void blockIp(String ip, long durationSeconds) {
        redisTemplate.opsForValue().set(
            "ip_block:" + ip,
            true,
            durationSeconds,
            TimeUnit.SECONDS
        );
    }
    
    public boolean isBlocked(String ip) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().get("ip_block:" + ip)
        );
    }
}

6. Content Moderation

@Service
public class ContentModerationService {
    
    private static final List<String> SPAM_KEYWORDS = Arrays.asList(
        "buy viagra", "casino", "lottery", "click here"
    );
    
    public boolean isSpam(String content) {
        String lowerContent = content.toLowerCase();
        
        // Проверка на известные спам-ключевые слова
        for (String keyword : SPAM_KEYWORDS) {
            if (lowerContent.contains(keyword)) {
                return true;
            }
        }
        
        // Проверка на избыточное количество ссылок
        int linkCount = countLinks(content);
        if (linkCount > 3) {
            return true;
        }
        
        // Проверка на капс (AAAA BBBB CCCC)
        double capsRatio = calculateCapsRatio(content);
        if (capsRatio > 0.7) {
            return true;
        }
        
        return false;
    }
    
    private int countLinks(String content) {
        return (int) content.split("(http|https)://", -1).length - 1;
    }
    
    private double calculateCapsRatio(String content) {
        long capsCount = content.chars().filter(Character::isUpperCase).count();
        long letterCount = content.chars().filter(Character::isLetter).count();
        return letterCount > 0 ? (double) capsCount / letterCount : 0;
    }
}

@PostMapping("/comments")
public ResponseEntity<?> addComment(@RequestBody CommentRequest request) {
    if (contentModerationService.isSpam(request.getText())) {
        return ResponseEntity.status(400)
            .body(new ErrorResponse("Your comment appears to be spam"));
    }
    
    // добавить комментарий
    return ResponseEntity.ok(new CommentResponse("Comment added"));
}

7. Двухфакторная аутентификация (2FA)

@Service
public class TwoFactorAuthService {
    
    @Autowired
    private SmsService smsService;
    
    public void sendOtpCode(String userId, String phoneNumber) {
        String otp = generateOtp();
        
        // Сохраняем OTP с expiry time
        redisTemplate.opsForValue().set(
            "otp:" + userId,
            otp,
            5,  // 5 минут
            TimeUnit.MINUTES
        );
        
        // Отправляем SMS
        smsService.sendSms(phoneNumber, "Your OTP: " + otp);
    }
    
    public boolean verifyOtp(String userId, String otp) {
        String storedOtp = redisTemplate.opsForValue().get("otp:" + userId);
        
        if (storedOtp == null || !storedOtp.equals(otp)) {
            return false;
        }
        
        // Удаляем OTP после успешной проверки
        redisTemplate.delete("otp:" + userId);
        return true;
    }
    
    private String generateOtp() {
        return String.format("%06d", new Random().nextInt(1000000));
    }
}

8. Комплексная стратегия

@Configuration
public class SpamPreventionConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(new IpBlockingFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .headers()
                .contentSecurityPolicy("default-src self");
        
        return http.build();
    }
}

// application.properties
spring.mvc.servlet.load-on-startup=1
server.servlet.session.timeout=30m
spring.security.filter.order=5

Лучшие практики

  1. Многослойная защита — используйте несколько методов одновременно
  2. Мониторинг — логируйте все подозрительные действия
  3. Проактивность — анализируйте паттерны спама
  4. Адаптивность — обновляйте правила при обнаружении новых типов спама
  5. Прозрачность — сообщайте пользователям о причинах блокировки
  6. Тестирование — регулярно проверяйте эффективность защиты
  7. Балансирование — не блокируйте легитимных пользователей

Комбинация Rate Limiting, CAPTCHA, Email Verification и Content Moderation обеспечивает надежную защиту от спама.