Перейти к содержанию
Innovialabs

Rate limiting и защита от перебора паролей для входа и API

Практика4 мин

Коротко

Ограничивайте число попыток сразу по IP и по аккаунту, вместо жёсткой блокировки используйте растущие задержки и CAPTCHA после нескольких неудач, а лимиты храните в общем хранилище вроде Redis и дублируйте базовым лимитом в nginx.

Короткий ответ

Rate limiting — ограничение числа запросов от одного источника за период времени. Для входа и API это главная защита от перебора паролей, подбора кодов из SMS и злоупотребления API.

Рабочая схема для большинства проектов:

  • Два счётчика: по IP и по аккаунту (логину, email, телефону).
  • После нескольких неудачных попыток — CAPTCHA или растущая задержка, а не вечная блокировка.
  • Лимиты хранятся в общем хранилище (Redis), чтобы работали на всех серверах.
  • Поверх — грубый лимит в nginx или CDN, который отсекает явный мусор до приложения.
  • Ответ при превышении — HTTP 429 с заголовком Retry-After.

Алгоритмы

АлгоритмКак работаетКогда подходит
Fixed windowСчётчик на интервал (например, минуту), обнуляется в начале новогоПросто и дёшево; на стыке окон возможен двойной всплеск
Sliding windowУчитывает запросы за последние N секунд, а не за календарную минутуТочнее для входа и чувствительных операций
Token bucket«Ведро» пополняется с постоянной скоростью, каждый запрос забирает токенAPI: допускает короткие всплески при средней ограниченной скорости
Leaky bucketЗапросы проходят с фиксированной скоростью, излишек ждёт в очереди или отбрасываетсяСглаживание нагрузки; так работает limit_req в nginx

Для формы входа обычно хватает fixed или sliding window, для публичного API — token bucket.

По IP или по аккаунту

  • Только по IP — злоумышленник перебирает пароль одного пользователя с тысяч адресов (ботнет, прокси) и не упирается в лимит. Плюс за одним IP могут сидеть целый офис или мобильный оператор.
  • Только по аккаунту — атакующий пробует один популярный пароль на тысячах аккаунтов (password spraying), и на каждом аккаунте всего одна попытка.

Поэтому нужны оба лимита: по аккаунту — строгий (несколько неудач за 15 минут), по IP — мягче и шире (десятки попыток в минуту), плюс общий лимит неудачных входов на весь сервис для обнаружения массовой атаки.

Не раскрывайте, существует ли аккаунт: сообщение об ошибке и время ответа должны быть одинаковыми для «нет такого пользователя» и «неверный пароль».

Блокировка или задержка

  • Жёсткая блокировка аккаунта после N ошибок проста, но опасна: любой может заблокировать чужой аккаунт, просто вводя неверный пароль. Это готовый инструмент для отказа в обслуживании.
  • Прогрессивная задержка — после каждой неудачи пауза растёт (1, 2, 4, 8 секунд...). Перебор становится бесполезным, а настоящий пользователь почти ничего не замечает.
  • Временная блокировка на 15-30 минут с уведомлением владельцу на почту — разумный компромисс.

Где ставить CAPTCHA

  • Не на первую попытку входа: это раздражает всех ради немногих.
  • Показывайте её после 2-3 неудач по аккаунту или IP, а также на регистрации, восстановлении пароля и отправке SMS.
  • Предпочитайте «невидимые» проверки, которые показывают задачу только подозрительным посетителям.
  • CAPTCHA — дополнение к лимитам, а не замена: её решают сервисы распознавания.

Реализация в nginx

Базовый слой — ограничить частоту запросов к входу с одного IP:

http {
    limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
    limit_req_status 429;

    server {
        location = /api/login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://app;
        }
    }
}

rate=10r/m — средняя скорость, burst=5 — допустимый короткий всплеск. Если сайт за CDN или балансировщиком, настройте модуль realip, иначе все запросы будут считаться с IP прокси.

Реализация на Redis

Счётчик по аккаунту в приложении. Lua-скрипт выполняется в Redis атомарно, поэтому счётчик не останется без срока жизни:

const script = `
  local c = redis.call("INCR", KEYS[1])
  if c == 1 then redis.call("EXPIRE", KEYS[1], ARGV[1]) end
  return c
`;

async function tooManyAttempts(login) {
  const key = `login:fail:${login.toLowerCase()}`;
  const count = await redis.eval(script, 1, key, 900); // окно 15 минут
  return count > 5;
}

Увеличивайте счётчик при неудачном входе, сбрасывайте при успешном. Такой же счётчик с ключом по IP — второй слой.

Частые ошибки

  • Лимит хранится в памяти процесса — при нескольких серверах он не работает.
  • Защищён только /login, но не восстановление пароля, проверка SMS-кода и API-токены.
  • Нет лимита на ввод одноразового кода: короткий цифровой код подбирается быстро.
  • Реальный IP клиента не определяется за прокси, и лимит блокирует всех сразу.

Частые вопросы

Универсальных цифр нет. Отталкивайтесь от поведения реальных пользователей: человек редко ошибается больше нескольких раз подряд. Начните со строгого лимита по аккаунту и мягкого по IP, затем смотрите логи и корректируйте.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.