bcrypt.js используется исключительно для безопасного хранения паролей и их проверки. Алгоритм обеспечивает вычислительную дороговизну сравнения хэшей, что делает массовый перебор паролей существенно менее эффективным.
Однако bcrypt сам по себе не защищает от попыток подбора пароля в прикладной логике. Он не ограничивает количество запросов, не управляет состоянием аккаунта и не предотвращает автоматизированные атаки на endpoint авторизации.
Следовательно, защита строится на нескольких независимых уровнях:
Rate limiting снижает эффективность brute force атак ещё до выполнения проверки пароля.
Типовые цели:
/loginНа практике применяются несколько моделей:
Фиксированное окно (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;
}
Rate limiting защищает канал, но не конкретный аккаунт. Account lockout добавляет защиту на уровне учётной записи.
Модель основана на отслеживании неудачных попыток входа:
Типовая схема хранения:
users:
id
email
password_hash
failed_attempts
lock_until
last_failed_login
Логика блокировки:
Пример логики:
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.compare — операция дорогая, поэтому порядок проверок критичен.
Оптимальная последовательность:
Пример 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
Несмотря на эффективность, механизм блокировки требует аккуратной настройки.
1. DoS на аккаунт Злоумышленник может намеренно блокировать чужие аккаунты.
2. User enumeration Различие ответов (blocked / wrong password) раскрывает существование аккаунта.
3. UX деградация Частые блокировки создают проблемы легитимным пользователям.
Типовая защита от enumeration:
return { success: false };
единый ответ независимо от причины ошибки.
В масштабируемых системах возникает проблема гонок:
Решения:
Redis atomic increment
await redis.incr(`fail:${userId}`);
или транзакции базы данных
UPD ATE users
SE T failed_attempts = failed_attempts + 1
WH ERE id = ?
Для lockout критично использовать атомарные операции:
bcrypt остаётся последним барьером. Его характеристики:
Но без rate limiting и lockout:
Эффективная архитектура строится как многоуровневая система:
Edge layer:
Application layer:
Account layer:
Crypto layer:
Такая структура снижает вероятность успешного brute force до минимального уровня при контролируемой нагрузке на систему.