Bcrypt для JavaScript (Node.js): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Bcrypt для JavaScript (Node.js): полное руководство

1. Что это и зачем нужна

Bcrypt — это адаптивная криптографическая хеш-функция, специально разработанная для безопасного хранения паролей. В отличие от обычных хеш-функций (MD5, SHA-1, SHA-256), которые созданы для быстрого вычисления, Bcrypt намеренно медленный и ресурсоёмкий. Это делает атаки перебором (brute-force) и радужными таблицами практически невыполнимыми. Библиотека bcrypt для Node.js предоставляет простой и надёжный интерфейс для хеширования и проверки паролей.

Зачем использовать Bcrypt? При регистрации пользователя на сайте его пароль никогда не должен храниться в открытом виде. Если база данных будет скомпрометирована, злоумышленник получит доступ ко всем паролям. Bcrypt решает эту проблему: вы хешируете пароль с уникальной «солью» (salt) и сохраняете только хеш. При входе пользователя вы повторно хешируете введённый пароль и сравниваете с сохранённым хешем. Bcrypt автоматически генерирует и встраивает соль в результат, поэтому вам не нужно хранить её отдельно.

Ключевая особенность Bcrypt — возможность регулировать «стоимость» (cost factor). Чем выше значение cost, тем больше времени требуется для вычисления хеша. С ростом вычислительных мощностей можно просто увеличить cost, и старые хеши останутся валидными (при проверке используется cost, сохранённый в хеше). Это делает Bcrypt устойчивым к будущему прогрессу в области аппаратного ускорения.

2. Установка

Для установки библиотеки Bcrypt в проекте Node.js выполните следующую команду в терминале (в корневой папке проекта):

npm install bcrypt

После установки вы можете импортировать библиотеку в свой код:

const bcrypt = require('bcrypt');

Если вы используете ES-модули (type: "module" в package.json), импорт будет выглядеть так:

import bcrypt from 'bcrypt';

3. Быстрый старт — минимальный рабочий пример

В этом примере мы хешируем пароль и затем проверяем его. Это основа для любой системы аутентификации.

const bcrypt = require('bcrypt');

const plainPassword = 'mySuperSecretPassword123!';
const saltRounds = 10;

// Хеширование пароля
bcrypt.hash(plainPassword, saltRounds, function(err, hash) {
    if (err) {
        console.error('Ошибка хеширования:', err);
        return;
    }
    console.log('Хеш пароля:', hash);

    // Проверка пароля
    bcrypt.compare(plainPassword, hash, function(err, result) {
        if (err) {
            console.error('Ошибка сравнения:', err);
            return;
        }
        console.log('Пароль совпадает:', result); // true
    });
});

Этот код делает две вещи: создаёт хеш пароля и затем проверяет, соответствует ли исходный пароль этому хешу. Обратите внимание, что saltRounds определяет сложность вычислений (рекомендуемое значение — 10–12).

4. Основные методы/функции/классы

4.1. bcrypt.hash(data, saltOrRounds, callback)

Сигнатура: bcrypt.hash(data: string | Buffer, saltOrRounds: number | string, callback: (err: Error | null, encrypted: string) => void): void

Что делает: Асинхронно хеширует строку или Buffer. saltOrRounds может быть числом (количество раундов) или готовой солью (строка). Возвращает хеш через callback.

Пример:

bcrypt.hash('password123', 12, (err, hash) => {
    if (err) throw err;
    console.log(hash); // $2b$12$...
});

4.2. bcrypt.hashSync(data, saltOrRounds)

Сигнатура: bcrypt.hashSync(data: string | Buffer, saltOrRounds: number | string): string

Что делает: Синхронная версия hash. Блокирует выполнение до завершения. Подходит для скриптов и тестов, но не для продакшн-серверов.

Пример:

const hash = bcrypt.hashSync('password123', 10);
console.log(hash);

4.3. bcrypt.compare(data, encrypted, callback)

Сигнатура: bcrypt.compare(data: string | Buffer, encrypted: string, callback: (err: Error | null, same: boolean) => void): void

Что делает: Сравнивает исходные данные с хешем. Возвращает true, если данные соответствуют хешу, иначе false.

Пример:

bcrypt.compare('password123', hash, (err, result) => {
    console.log(result); // true или false
});

4.4. bcrypt.compareSync(data, encrypted)

Сигнатура: bcrypt.compareSync(data: string | Buffer, encrypted: string): boolean

Что делает: Синхронная версия compare. Возвращает true или false.

Пример:

const isMatch = bcrypt.compareSync('password123', hash);
console.log(isMatch);

4.5. bcrypt.genSalt(rounds, callback)

Сигнатура: bcrypt.genSalt(rounds: number, callback: (err: Error | null, salt: string) => void): void

Что делает: Генерирует криптографически стойкую соль с указанным количеством раундов. Обычно используется вместе с hash, но можно и отдельно.

Пример:

bcrypt.genSalt(12, (err, salt) => {
    console.log(salt); // $2b$12$...
});

4.6. bcrypt.genSaltSync(rounds)

Сигнатура: bcrypt.genSaltSync(rounds: number): string

Что делает: Синхронная генерация соли.

Пример:

const salt = bcrypt.genSaltSync(10);
console.log(salt);

4.7. bcrypt.getRounds(encrypted)

Сигнатура: bcrypt.getRounds(encrypted: string): number

Что делает: Извлекает количество раундов (cost factor) из готового хеша. Полезно для проверки, с какой сложностью был создан хеш.

Пример:

const hash = '$2b$12$LJ3m...';
const rounds = bcrypt.getRounds(hash);
console.log(rounds); // 12

4.8. bcrypt.getSalt(encrypted)

Сигнатура: bcrypt.getSalt(encrypted: string): string

Что делает: Извлекает соль из готового хеша. Соль — это первые 29 символов хеша (включая префикс).

Пример:

const hash = '$2b$12$LJ3m...';
const salt = bcrypt.getSalt(hash);
console.log(salt); // $2b$12$LJ3m... (первые 29 символов)

4.9. bcrypt.hash(data, saltOrRounds) (Promise-версия)

Сигнатура: bcrypt.hash(data: string | Buffer, saltOrRounds: number | string): Promise<string>

Что делает: Начиная с версии 5.0.0, bcrypt поддерживает Promises. Если не передать callback, функция вернёт Promise.

Пример:

async function hashPassword(password) {
    const hash = await bcrypt.hash(password, 10);
    return hash;
}

4.10. bcrypt.compare(data, encrypted) (Promise-версия)

Сигнатура: bcrypt.compare(data: string | Buffer, encrypted: string): Promise<boolean>

Что делает: Promise-версия сравнения.

Пример:

async function checkPassword(password, hash) {
    const isMatch = await bcrypt.compare(password, hash);
    return isMatch;
}

5. Реальный практический пример (100–200 строк кода)

Ниже представлен полноценный пример регистрации и входа пользователя с использованием Express.js и Bcrypt. Код включает в себя обработку ошибок, валидацию и демонстрацию всех основных методов.

// app.js
const express = require('express');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// Имитация базы данных (в реальности используйте MongoDB, PostgreSQL и т.д.)
const users = [];

// Middleware для логирования
app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

// Регистрация пользователя
app.post('/register', async (req, res) => {
    try {
        const { username, password } = req.body;

        // Валидация
        if (!username || !password) {
            return res.status(400).json({ error: 'Username and password are required' });
        }
        if (password.length < 6) {
            return res.status(400).json({ error: 'Password must be at least 6 characters' });
        }

        // Проверка, существует ли пользователь
        const existingUser = users.find(u => u.username === username);
        if (existingUser) {
            return res.status(409).json({ error: 'User already exists' });
        }

        // Хеширование пароля с cost factor 12
        const saltRounds = 12;
        const hashedPassword = await bcrypt.hash(password, saltRounds);

        // Сохраняем пользователя
        const newUser = {
            id: users.length + 1,
            username,
            passwordHash: hashedPassword,
            createdAt: new Date().toISOString()
        };
        users.push(newUser);

        console.log(`User registered: ${username}`);
        res.status(201).json({
            message: 'User created successfully',
            userId: newUser.id
        });
    } catch (error) {
        console.error('Registration error:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

// Вход пользователя
app.post('/login', async (req, res) => {
    try {
        const { username, password } = req.body;

        if (!username || !password) {
            return res.status(400).json({ error: 'Username and password are required' });
        }

        // Ищем пользователя
        const user = users.find(u => u.username === username);
        if (!user) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        // Сравниваем пароль с хешем
        const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
        if (!isPasswordValid) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        // Успешный вход (в реальности здесь бы создавался JWT токен)
        console.log(`User logged in: ${username}`);
        res.json({
            message: 'Login successful',
            userId: user.id,
            username: user.username
        });
    } catch (error) {
        console.error('Login error:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

// Получение информации о хеше (демонстрация getRounds и getSalt)
app.get('/hash-info/:userId', (req, res) => {
    const user = users.find(u => u.id === parseInt(req.params.userId));
    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    const hash = user.passwordHash;
    const rounds = bcrypt.getRounds(hash);
    const salt = bcrypt.getSalt(hash);

    res.json({
        username: user.username,
        hash: hash.substring(0, 30) + '...', // Показываем только часть
        rounds: rounds,
        salt: salt,
        createdAt: user.createdAt
    });
});

// Синхронный пример (для демонстрации)
app.get('/sync-example', (req, res) => {
    const start = Date.now();
    const hash = bcrypt.hashSync('testPassword', 10);
    const end = Date.now();
    res.json({
        hash: hash,
        timeMs: end - start,
        note: 'This is a synchronous example, not recommended for production'
    });
});

// Запуск сервера
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
    console.log('Test with:');
    console.log('  POST /register - {"username": "john", "password": "secret123"}');
    console.log('  POST /login - {"username": "john", "password": "secret123"}');
    console.log('  GET /hash-info/1');
});

6. Советы по производительности и типичные ошибки

Советы по производительности

  • Используйте асинхронные методы в продакшне: bcrypt.hash и bcrypt.compare являются асинхронными и не блокируют цикл событий Node.js. Синхронные версии (hashSync, compareSync) блокируют поток и могут серьёзно снизить производительность под нагрузкой.
  • Выбирайте оптимальный cost factor: Рекомендуемое значение — 10–12. Слишком низкое значение (менее 8) делает хеш

Рекомендаций