Rate limiting и account lockout в связке с bcrypt

Роль bcrypt.js в системе аутентификации

bcrypt.js используется исключительно для безопасного хранения паролей и их проверки. Алгоритм обеспечивает вычислительную дороговизну сравнения хэшей, что делает массовый перебор паролей существенно менее эффективным.

Однако bcrypt сам по себе не защищает от попыток подбора пароля в прикладной логике. Он не ограничивает количество запросов, не управляет состоянием аккаунта и не предотвращает автоматизированные атаки на endpoint авторизации.

Следовательно, защита строится на нескольких независимых уровнях:

  • ограничение частоты запросов (rate limiting)
  • блокировка аккаунта при подозрительной активности (account lockout)
  • корректная интеграция с bcrypt.compare
  • контроль распределённых состояний в инфраструктуре

Ограничение частоты запросов как первая линия защиты

Rate limiting снижает эффективность brute force атак ещё до выполнения проверки пароля.

Типовые цели:

  • ограничение количества запросов к /login
  • защита от credential stuffing
  • снижение нагрузки на bcrypt (дорогая операция)
  • предотвращение перебора из одного IP или токена

На практике применяются несколько моделей:

Фиксированное окно (fixed window) Простой счётчик запросов за интервал времени.

Скользящее окно (sliding window) Более точная модель с учётом времени каждого запроса.

Token bucket Гибкая модель, позволяющая кратковременные всплески.

Leaky bucket Сглаживание нагрузки с постоянной скоростью обработки.


Пример реализации через Express middleware:

import rateLimit fr om 'express-rate-limit';

export const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: 'Too many login attempts'
});

В распределённых системах локальные лимитеры недостаточны. Используется Redis:

import Redis fr om 'ioredis';

const redis = new Redis();

async function isRateLimited(ip) {
  const key = `rl:login:${ip}`;
  const count = await redis.incr(key);

  if (count === 1) {
    await redis.expire(key, 900);
  }

  return count > 20;
}

Account lockout как защита на уровне пользователя

Rate limiting защищает канал, но не конкретный аккаунт. Account lockout добавляет защиту на уровне учётной записи.

Модель основана на отслеживании неудачных попыток входа:

  • счётчик failed_attempts
  • временная блокировка lock_until
  • сброс после успешной аутентификации

Типовая схема хранения:

users:
  id
  email
  password_hash
  failed_attempts
  lock_until
  last_failed_login

Логика блокировки:

  • при каждой неудачной попытке увеличивается счётчик
  • при достижении порога устанавливается lock_until
  • успешный вход сбрасывает счётчики

Пример логики:

const MAX_ATTEMPTS = 5;
const LOCK_TIME = 15 * 60 * 1000;

async function registerFailedLogin(user) {
  user.failed_attempts += 1;
  user.last_failed_login = new Date();

  if (user.failed_attempts >= MAX_ATTEMPTS) {
    user.lock_until = Date.now() + LOCK_TIME;
  }

  await user.save();
}

Проверка блокировки:

function isLocked(user) {
  return user.lock_until && user.lock_until > Date.now();
}

Интеграция с bcrypt.js в процессе аутентификации

bcrypt.compare — операция дорогая, поэтому порядок проверок критичен.

Оптимальная последовательность:

  1. Проверка rate lim it
  2. Проверка lockout состояния
  3. Получение пользователя
  4. bcrypt.compare
  5. обновление состояния

Пример login flow:

import bcrypt from 'bcryptjs';

async function login(email, password, req) {
  const ip = req.ip;

  if (await isRateLimited(ip)) {
    return { success: false };
  }

  const user = await Users.findOne({ email });

  if (!user) {
    return { success: false };
  }

  if (isLocked(user)) {
    return { success: false };
  }

  const match = await bcrypt.compare(password, user.password_hash);

  if (!match) {
    await registerFailedLogin(user);
    return { success: false };
  }

  user.failed_attempts = 0;
  user.lock_until = null;
  await user.save();

  return { success: true };
}

Причины комбинирования rate limiting и account lockout

Каждый механизм закрывает отдельный класс атак:

Rate limiting

  • защита от распределённых атак
  • ограничение автоматизированных запросов
  • снижение нагрузки на сервер и bcrypt

Account lockout

  • защита конкретного аккаунта
  • предотвращение перебора одного пользователя
  • реакция на targeted attack

Риски и побочные эффекты account lockout

Несмотря на эффективность, механизм блокировки требует аккуратной настройки.

1. DoS на аккаунт Злоумышленник может намеренно блокировать чужие аккаунты.

2. User enumeration Различие ответов (blocked / wrong password) раскрывает существование аккаунта.

3. UX деградация Частые блокировки создают проблемы легитимным пользователям.


Типовая защита от enumeration:

return { success: false };

единый ответ независимо от причины ошибки.


Распределённые системы и race conditions

В масштабируемых системах возникает проблема гонок:

  • несколько запросов одновременно увеличивают failed_attempts
  • возможна потеря инкремента
  • lock_until может быть перезаписан некорректно

Решения:

Redis atomic increment

await redis.incr(`fail:${userId}`);

или транзакции базы данных

UPD ATE users
SE T failed_attempts = failed_attempts + 1
WH ERE id = ?

Для lockout критично использовать атомарные операции:

  • Redis Lua scripts
  • database transactions
  • optimistic locking (version field)

Устойчивость bcrypt в цепочке защиты

bcrypt остаётся последним барьером. Его характеристики:

  • высокая вычислительная стоимость
  • адаптивный cost factor
  • устойчивость к GPU ускорению (частично)

Но без rate limiting и lockout:

  • bcrypt просто замедляет атаку, но не останавливает её
  • ресурсы сервера расходуются на бесполезные вычисления

Комбинированная модель защиты

Эффективная архитектура строится как многоуровневая система:

  1. Edge layer:

    • CDN / WAF
    • IP filtering
  2. Application layer:

    • rate limiting
    • captcha (при необходимости)
  3. Account layer:

    • lockout
    • failed attempt tracking
  4. Crypto layer:

    • bcrypt hashing and verification

Такая структура снижает вероятность успешного brute force до минимального уровня при контролируемой нагрузке на систему.