Архитектура приложения, в которой операции с паролями размазаны по контроллерам, роутам и вспомогательным функциям, быстро становится неконтролируемой. Хеширование паролей — одна из тех зон ответственности, которые требуют строгой изоляции, повторного использования и централизованного контроля. Использование библиотеки password-hash в JavaScript логично выносится в отдельный сервисный слой, чтобы отделить бизнес-логику от инфраструктурных деталей.
Сервисный слой выступает промежуточным уровнем между HTTP-слоем (контроллерами) и слоями доступа к данным. Его задача — инкапсулировать правила обработки данных, включая создание и проверку хешей паролей, работу с солью, а также унификацию интерфейса для других частей системы.
Хеширование пароля — это не просто преобразование строки, а критически важная операция безопасности. Прямое использование алгоритмов в контроллерах приводит к нескольким проблемам:
Сервисный слой решает эти проблемы за счёт того, что вся работа с паролями концентрируется в одном месте.
Библиотека 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
Основная задача — изолировать библиотеку и предоставить бизнес-ориентированные методы.
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 отвечает за бизнес-операции с пользователями и использует 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);
});
Сервис должен быть устойчив к некорректному вводу. Это особенно важно, поскольку он используется в разных контекстах приложения.
Основные меры:
Сервисный слой позволяет изолировать детали реализации:
Это важно, поскольку криптографические зависимости со временем устаревают, и их замена не должна затрагивать бизнес-логику.
Со временем могут появиться новые требования:
Сервисный слой становится естественным местом для этих расширений.
Пример расширения:
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;
}
}
Такой подход позволяет подменять реализацию хеширования в тестах или при миграции алгоритмов без изменения бизнес-кода.
Если хеширование выполняется прямо в контроллерах, возникают следующие проблемы:
Сервисный слой становится не просто архитектурным элементом, а частью модели безопасности приложения. Он определяет:
Это превращает его в критически важный компонент системы, а не просто вспомогательный модуль.