Комментарии (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
Лучшие практики
- Многослойная защита — используйте несколько методов одновременно
- Мониторинг — логируйте все подозрительные действия
- Проактивность — анализируйте паттерны спама
- Адаптивность — обновляйте правила при обнаружении новых типов спама
- Прозрачность — сообщайте пользователям о причинах блокировки
- Тестирование — регулярно проверяйте эффективность защиты
- Балансирование — не блокируйте легитимных пользователей
Комбинация Rate Limiting, CAPTCHA, Email Verification и Content Moderation обеспечивает надежную защиту от спама.