Слой работы с паролями: где расположить логику хеширования

Работа с паролями относится к критически важной части системы аутентификации, и выбор слоя, в котором выполняется хеширование, напрямую влияет на безопасность, тестируемость и масштабируемость приложения. Использование библиотеки bcrypt.js в JavaScript предполагает не просто технический вызов функции hash, а правильное распределение ответственности между слоями архитектуры.

Хеширование пароля должно быть строго ограничено инфраструктурным или сервисным уровнем, не затрагивая контроллеры, маршруты или клиентскую часть. Основная причина заключается в необходимости изоляции криптографической логики от бизнес-логики и транспортного слоя.


Базовый принцип: пароль как часть доменной модели, хеш как инфраструктурная деталь

В доменной модели системы пароль существует как сущность только до момента его обработки. После этого он перестаёт существовать в исходном виде и заменяется хешем.

Доменные правила:

  • пароль принимается как входное значение
  • пароль не хранится и не передаётся дальше после обработки
  • сравнение всегда происходит через хеш

Инфраструктурные правила:

  • хеширование выполняется через специализированный модуль
  • используется адаптивный алгоритм (bcrypt)
  • параметр стоимости (salt rounds) централизован

Такое разделение позволяет сохранять доменный слой независимым от криптографических деталей.


Почему нельзя размещать bcrypt в контроллерах

Контроллеры должны заниматься исключительно обработкой HTTP-запросов и маршрутизацией данных. Размещение логики хеширования в контроллере приводит к нескольким проблемам:

  1. Нарушение SRP (Single Responsibility Principle) Контроллер начинает выполнять криптографические операции, что выходит за рамки его ответственности.

  2. Сложность тестирования Мокирование bcrypt внутри контроллера усложняет unit-тесты.

  3. Повторяемость кода При наличии нескольких точек регистрации логика хеширования дублируется.

  4. Утечка инфраструктурных деталей Контроллер начинает зависеть от конкретной реализации криптографии.


Рекомендуемый слой: сервис аутентификации

Оптимальное место для работы с bcrypt.js — сервисный слой (AuthService, UserService).

Пример структуры:

/controllers
/services
/repositories
/utils

Сервис отвечает за бизнес-логику, включая:

  • создание пользователя
  • хеширование пароля
  • проверку пароля
  • генерацию токенов

Пример реализации сервиса

import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12;

export class AuthService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async register(email, password) {
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new Error('User already exists');
    }

    const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

    const user = await this.userRepository.create({
      email,
      password: hashedPassword,
    });

    return user;
  }

  async validateUser(email, password) {
    const user = await this.userRepository.findByEmail(email);
    if (!user) return null;

    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) return null;

    return user;
  }
}

Почему хеширование не должно быть в репозитории

Репозиторий отвечает за доступ к данным, а не за их трансформацию. Если поместить bcrypt в слой хранения данных, возникает ряд архитектурных проблем:

  • база данных становится зависимой от криптографии
  • невозможность смены алгоритма без изменения слоя хранения
  • нарушение принципа разделения ответственности

Репозиторий должен принимать уже готовые данные:

create(user)
update(user)
findByEmail(email)

Middleware и хеширование: антипаттерн

Иногда хеширование пытаются разместить в middleware, особенно в Express-подобных фреймворках. Это приводит к скрытой логике:

  • пароль может быть изменён до попадания в сервис
  • усложняется трассировка данных
  • нарушается предсказуемость потока выполнения

Middleware допустим только для:

  • логирования
  • валидации формата данных
  • аутентификации токенов

Клиентская сторона и bcrypt

Использование bcrypt.js на клиенте считается небезопасным и архитектурно неверным решением.

Причины:

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

Пароль должен передаваться по защищённому каналу (HTTPS) в сыром виде и хешироваться только на сервере.


Управление salt rounds и централизованная конфигурация

Параметр salt rounds определяет вычислительную стоимость хеширования.

Рекомендуемая практика:

config/security.js
export const SECURITY_CONFIG = {
  bcryptSaltRounds: 12,
};

Сервис использует конфигурацию:

bcrypt.hash(password, SECURITY_CONFIG.bcryptSaltRounds);

Преимущества:

  • централизованное управление безопасностью
  • возможность масштабирования нагрузки
  • упрощение A/B тестирования параметров

Сравнение пароля: где должна происходить операция

Сравнение выполняется строго в сервисном слое:

bcrypt.compare(plainPassword, hashedPassword);

Недопустимые места:

  • контроллер
  • middleware
  • клиент
  • база данных (через триггеры)

Асинхронность и влияние на архитектуру

bcrypt является CPU-intensive операцией. Использование bcrypt.js в Node.js требует асинхронного подхода.

Последствия игнорирования:

  • блокировка event loop
  • деградация производительности API
  • задержки при высокой нагрузке

Правильный подход:

  • всегда использовать await bcrypt.hash
  • избегать синхронных версий
  • выносить операции в сервисный слой, где проще контролировать нагрузку

Ошибки архитектуры при работе с паролями

Распространённые антишаблоны:

  1. Хеширование в контроллере
  2. Хранение сырого пароля в промежуточных слоях
  3. Использование одинакового salt rounds без конфигурации
  4. Повторное хеширование уже захешированного пароля
  5. Сравнение строк вместо bcrypt.compare

Правильный поток данных в системе

Типичный жизненный цикл пароля:

  1. Клиент отправляет пароль через HTTPS
  2. Контроллер передаёт данные в сервис
  3. Сервис вызывает bcrypt.hash
  4. Репозиторий сохраняет хеш
  5. При логине сервис использует bcrypt.compare
  6. Результат возвращается контроллеру

Изоляция криптографии как архитектурное требование

Использование bcrypt.js должно быть скрыто за абстракцией сервиса. Это позволяет:

  • заменить bcrypt на argon2 без изменения контроллеров
  • тестировать бизнес-логику без криптографии
  • централизовать безопасность
  • упростить сопровождение системы

Связь с чистой архитектурой

В терминах Clean Architecture:

  • Entities: пользователь без пароля в открытом виде
  • Use Cases: регистрация, аутентификация
  • Interface Adapters: контроллеры
  • Frameworks & Drivers: bcrypt.js

bcrypt относится к внешнему слою и не должен проникать в ядро системы.