Разделение логики хеширования через сервисный слой

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

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


Роль хеширования паролей в архитектуре приложения

Хеширование пароля — это не просто преобразование строки, а критически важная операция безопасности. Прямое использование алгоритмов в контроллерах приводит к нескольким проблемам:

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

Сервисный слой решает эти проблемы за счёт того, что вся работа с паролями концентрируется в одном месте.


Библиотека password-hash как инфраструктурный компонент

Библиотека password-hash предоставляет простой API для генерации хешей и проверки паролей. Обычно она поддерживает:

  • генерацию хеша с солью;
  • проверку пароля по сохранённому хешу;
  • использование различных алгоритмов внутри (в зависимости от конфигурации).

Пример базового использования:

const passwordHash = require('password-hash');

const hash = passwordHash.generate('mySuperPassword');
const verified = passwordHash.verify('mySuperPassword', hash);

Несмотря на простоту API, прямое использование этой библиотеки в контроллерах нарушает принцип разделения ответственности.


Формирование сервисного слоя для работы с паролями

Сервисный слой выступает обёрткой над password-hash и предоставляет единый интерфейс для всей системы.

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

services/
  passwordService.js
  userService.js
controllers/
  authController.js
repositories/
  userRepository.js

Реализация PasswordService

Основная задача — изолировать библиотеку и предоставить бизнес-ориентированные методы.

const passwordHash = require('password-hash');

class PasswordService {
    hashPassword(plainPassword) {
        if (!plainPassword || typeof plainPassword !== 'string') {
            throw new Error('Invalid password format');
        }

        return passwordHash.generate(plainPassword);
    }

    verifyPassword(plainPassword, hashedPassword) {
        if (!plainPassword || !hashedPassword) {
            return false;
        }

        return passwordHash.verify(plainPassword, hashedPassword);
    }
}

module.exports = new PasswordService();

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


Интеграция сервиса в UserService

UserService отвечает за бизнес-операции с пользователями и использует PasswordService как зависимость.

const passwordService = require('./passwordService');
const userRepository = require('../repositories/userRepository');

class UserService {
    async registerUser(email, password) {
        const existingUser = await userRepository.findByEmail(email);

        if (existingUser) {
            throw new Error('User already exists');
        }

        const hashedPassword = passwordService.hashPassword(password);

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

        return user;
    }

    async validateUser(email, password) {
        const user = await userRepository.findByEmail(email);

        if (!user) {
            return null;
        }

        const isValid = passwordService.verifyPassword(password, user.password);

        if (!isValid) {
            return null;
        }

        return user;
    }
}

module.exports = new UserService();

Контроллер без логики хеширования

Контроллер должен оставаться максимально «тонким» и не содержать криптографических операций.

const userService = require('../services/userService');

class AuthController {
    async register(req, res) {
        try {
            const { email, password } = req.body;

            const user = await userService.registerUser(email, password);

            res.status(201).json(user);
        } catch (error) {
            res.status(400).json({ message: error.message });
        }
    }

    async login(req, res) {
        const { email, password } = req.body;

        const user = await userService.validateUser(email, password);

        if (!user) {
            return res.status(401).json({ message: 'Invalid credentials' });
        }

        res.json({ message: 'Success', userId: user.id });
    }
}

module.exports = new AuthController();

Преимущества вынесения хеширования в сервисный слой

Централизация логики даёт несколько ключевых преимуществ:

1. Единая точка изменения алгоритма

Если необходимо заменить password-hash на bcrypt или argon2, изменения вносятся только в PasswordService.

2. Улучшенная тестируемость

Сервис можно тестировать отдельно, без HTTP-слоя и базы данных.

3. Повышенная безопасность

Контроллеры не работают с паролями напрямую, что снижает риск логических ошибок и утечек.

4. Повторное использование

Один и тот же сервис используется в регистрации, авторизации, смене пароля и административных операциях.


Тестирование сервисного слоя

Сервисный слой удобно покрывается unit-тестами без инфраструктурных зависимостей.

const PasswordService = require('../services/passwordService');

test('should hash and verify password correctly', () => {
    const password = 'secure123';

    const hash = PasswordService.hashPassword(password);

    const isValid = PasswordService.verifyPassword(password, hash);

    expect(isValid).toBe(true);
});

Обработка ошибок и защита от некорректных данных

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

Основные меры:

  • проверка типа входных данных;
  • защита от пустых значений;
  • возврат безопасных значений вместо выброса ошибок там, где это допустимо;
  • унификация формата ошибок.

Изоляция криптографической логики

Сервисный слой позволяет изолировать детали реализации:

  • алгоритм хеширования;
  • формат соли;
  • параметры генерации;
  • версия библиотеки.

Это важно, поскольку криптографические зависимости со временем устаревают, и их замена не должна затрагивать бизнес-логику.


Расширение сервиса для будущих требований

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

  • поддержка миграции хешей (например, rehash при входе пользователя);
  • политика сложности паролей;
  • ограничение повторного использования старых паролей;
  • аудит попыток входа.

Сервисный слой становится естественным местом для этих расширений.

Пример расширения:

rehashIfNeeded(password, currentHash) {
    if (passwordHash.isDeprecated(currentHash)) {
        return this.hashPassword(password);
    }
    return currentHash;
}

Контроль зависимости и инверсия управления

При росте системы PasswordService может быть внедрён через DI-контейнер, что ещё больше усиливает слабую связанность модулей.

class UserService {
    constructor(passwordService, userRepository) {
        this.passwordService = passwordService;
        this.userRepository = userRepository;
    }
}

Такой подход позволяет подменять реализацию хеширования в тестах или при миграции алгоритмов без изменения бизнес-кода.


Ошибки типичной архитектуры без сервисного слоя

Если хеширование выполняется прямо в контроллерах, возникают следующие проблемы:

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

Структурная роль сервисного слоя в системе безопасности

Сервисный слой становится не просто архитектурным элементом, а частью модели безопасности приложения. Он определяет:

  • как создаются пароли;
  • как они проверяются;
  • как они обновляются;
  • как обеспечивается совместимость версий хешей.

Это превращает его в критически важный компонент системы, а не просто вспомогательный модуль.