Использование async/await с bcrypt.js

Библиотека bcrypt.js изначально проектировалась с поддержкой асинхронного API на основе callback-функций. При использовании современного JavaScript-стека предпочтение отдается async/await, что требует оборачивания callback-методов в Promise-слой.

Основные функции библиотеки:

  • bcrypt.hash(password, saltRounds, callback)
  • bcrypt.compare(password, hash, callback)
  • bcrypt.genSalt(saltRounds, callback)

Нативно Promise-версии отсутствуют, поэтому интеграция с async/await выполняется через util.promisify или ручную обертку.


Промисификация bcrypt.js

Стандартный способ адаптации библиотеки под async/await — использование util.promisify.

const bcrypt = require('bcryptjs');
const { promisify } = require('util');

const hashAsync = promisify(bcrypt.hash);
const compareAsync = promisify(bcrypt.compare);
const genSaltAsync = promisify(bcrypt.genSalt);

После преобразования функции становятся совместимыми с await:

const salt = await genSaltAsync(10);
const hash = await hashAsync('password123', salt);
const isValid = await compareAsync('password123', hash);

Такой подход сохраняет производительность неблокирующих операций и позволяет использовать линейную структуру кода.


Асинхронное хеширование паролей

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

Пример асинхронного хеширования:

const bcrypt = require('bcryptjs');
const { promisify } = require('util');

const hashAsync = promisify(bcrypt.hash);
const genSaltAsync = promisify(bcrypt.genSalt);

async function createPasswordHash(password) {
    const saltRounds = 12;
    const salt = await genSaltAsync(saltRounds);
    const hash = await hashAsync(password, salt);
    return hash;
}

Количество saltRounds определяет вычислительную сложность. Значения 10–12 считаются балансом между безопасностью и производительностью.


Проверка пароля через async/await

Сравнение пароля с хешем также выполняется асинхронно:

const bcrypt = require('bcryptjs');
const { promisify } = require('util');

const compareAsync = promisify(bcrypt.compare);

async function verifyPassword(password, hash) {
    const match = await compareAsync(password, hash);
    return match;
}

Внутри bcrypt.compare происходит извлечение соли из хеша и повторное вычисление значения, что делает процесс устойчивым к утечкам исходного значения.


Централизованная обработка ошибок

Асинхронная модель требует обработки ошибок через try/catch, так как любые сбои в вычислении хеша или сравнения приводят к исключениям Promise.

async function safeHash(password) {
    try {
        const salt = await genSaltAsync(10);
        return await hashAsync(password, salt);
    } catch (err) {
        throw new Error('Ошибка при генерации хеша');
    }
}

Типовые причины ошибок:

  • некорректный входной тип данных
  • превышение лимита длины строки
  • сбой криптографического вычисления

Параллелизм при массовой обработке паролей

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

async function hashUsers(users) {
    return await Promise.all(
        users.map(async (user) => {
            const hash = await hashAsync(user.password, 10);
            return {
                id: user.id,
                hash
            };
        })
    );
}

Promise.all позволяет выполнять операции параллельно, однако при высокой нагрузке возможна перегрузка event loop, поэтому в production-системах часто применяется ограничение concurrency.


Контроль нагрузки через последовательное выполнение

Для снижения нагрузки используется последовательная обработка:

async function hashUsersSequential(users) {
    const result = [];

    for (const user of users) {
        const hash = await hashAsync(user.password, 10);
        result.push({
            id: user.id,
            hash
        });
    }

    return result;
}

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


Использование async/await без promisify

В некоторых проектах создается собственная Promise-обертка:

function hash(password, saltRounds) {
    return new Promise((resolve, reject) => {
        bcrypt.hash(password, saltRounds, (err, hash) => {
            if (err) reject(err);
            else resolve(hash);
        });
    });
}

function compare(password, hash) {
    return new Promise((resolve, reject) => {
        bcrypt.compare(password, hash, (err, res) => {
            if (err) reject(err);
            else resolve(res);
        });
    });
}

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


Поведение event loop при bcrypt.js

bcrypt выполняет CPU-интенсивные операции, которые выносятся из основного потока через внутренние механизмы thread pool (в случае Node.js). Это позволяет сохранять отзывчивость приложения даже при активном хешировании.

При увеличении saltRounds растет нагрузка на процессор, что напрямую влияет на latency запросов.


Оптимизация работы с async bcrypt

Ключевые практики оптимизации:

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

Интеграция в типичный сервис аутентификации

const { promisify } = require('util');
const bcrypt = require('bcryptjs');

const hashAsync = promisify(bcrypt.hash);
const compareAsync = promisify(bcrypt.compare);

class AuthService {
    async register(password) {
        const hash = await hashAsync(password, 12);
        return hash;
    }

    async login(password, hash) {
        return await compareAsync(password, hash);
    }
}

Такая структура отделяет криптографическую логику от бизнес-слоя и упрощает тестирование.


Особенности поведения при высокой нагрузке

При интенсивном использовании bcrypt.js наблюдаются следующие эффекты:

  • рост времени ответа при увеличении saltRounds
  • увеличение нагрузки на CPU без блокировки event loop
  • необходимость балансировки между безопасностью и производительностью
  • влияние очередей Promise на задержки при массовых операциях

Архитектурно часто применяется вынесение хеширования в отдельные worker-процессы или микросервисы.