Интеграция с MySQL и SQLite

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

Библиотека password-hash в Node.js применяется для создания хешей и последующей верификации без необходимости прямого управления солью и алгоритмом на уровне приложения. При интеграции с MySQL и SQLite ключевым становится правильное проектирование схемы хранения и контроль процесса аутентификации.

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

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

const hashedPassword = passwordHash.generate('user_password');

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

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

const isValid = passwordHash.verify('user_password', hashedPassword);

Структура хранения данных пользователей

Независимо от СУБД, минимальная схема включает идентификатор пользователя и поле для хранения хеша.

MySQL

CRE ATE   TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

SQLite

CRE ATE   TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Поле password_hash хранит результат работы password-hash.generate() без изменений.

Интеграция с MySQL через mysql2

Для работы с MySQL обычно используется библиотека mysql2, поддерживающая промисы.

Установка:

npm install mysql2 password-hash

Подключение и создание пользователя:

const mysql = require('mysql2/promise');
const passwordHash = require('password-hash');

const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    database: 'app',
    password: 'secret'
});

async function createUser(email, password) {
    const hash = passwordHash.generate(password);

    await pool.execute(
        'INS ERT IN TO users (email, password_hash) VALUES (?, ?)',
        [email, hash]
    );
}

Проверка пользователя в MySQL

Процесс аутентификации строится на выборке хеша и его проверке:

async function authenticate(email, password) {
    const [rows] = await pool.execute(
        'SEL ECT password_hash FR OM users WHERE email = ?',
        [email]
    );

    if (rows.length === 0) {
        return false;
    }

    const hash = rows[0].password_hash;

    return passwordHash.verify(password, hash);
}

Ключевой момент — отсутствие логики сравнения на уровне SQL. База данных возвращает только сохранённое значение.

Интеграция с SQLite

SQLite часто используется в локальных приложениях, прототипах и десктопных решениях. На практике применяются sqlite3 или better-sqlite3.

Пример с sqlite3:

npm install sqlite3 password-hash

Инициализация:

const sqlite3 = require('sqlite3').verbose();
const passwordHash = require('password-hash');

const db = new sqlite3.Database('./database.db');

Создание пользователя:

function createUser(email, password) {
    const hash = passwordHash.generate(password);

    db.run(
        'INS ERT IN TO users (email, password_hash) VALUES (?, ?)',
        [email, hash]
    );
}

Проверка пользователя в SQLite

Так как sqlite3 работает асинхронно через callback-модель, проверка выполняется через get:

function authenticate(email, password, callback) {
    db.get(
        'SEL ECT password_hash FR OM users WHERE email = ?',
        [email],
        (err, row) => {
            if (err || !row) {
                return callback(false);
            }

            const isValid = passwordHash.verify(password, row.password_hash);
            callback(isValid);
        }
    );
}

Использование prepared statements

Во всех сценариях обязательным является применение параметризованных запросов. Это исключает SQL-инъекции на этапе передачи email и других пользовательских данных.

Пример корректного подхода:

'SEL ECT password_hash FR OM users WHERE email = ?'

Недопустимо формировать SQL через конкатенацию строк:

// небезопасно
"SEL ECT * FR OM users WHERE email = '" + email + "'"

Обновление пароля

При смене пароля выполняется перегенерация хеша:

MySQL

async function updatePassword(email, newPassword) {
    const hash = passwordHash.generate(newPassword);

    await pool.execute(
        'UPD ATE users SE T password_hash = ? WHERE email = ?',
        [hash, email]
    );
}

SQLite

function updatePassword(email, newPassword) {
    const hash = passwordHash.generate(newPassword);

    db.run(
        'UPD ATE users SE T password_hash = ? WHERE email = ?',
        [hash, email]
    );
}

Поведение хеша и обратная совместимость

Строка, создаваемая password-hash, включает метаданные алгоритма. Это позволяет системе корректно обрабатывать проверку даже при изменении параметров генерации в будущих версиях приложения.

Формат хеша обычно выглядит как структурированная строка, содержащая:

  • используемый алгоритм
  • соль
  • итоговое значение

Это избавляет от необходимости хранить дополнительные поля в базе данных.

Миграция и изменение схемы

При переходе на более строгие алгоритмы хеширования не требуется изменение структуры таблицы. Все изменения происходят на уровне приложения.

Возможные сценарии:

  • переход на более длинное поле TEXT вместо VARCHAR
  • добавление поля password_updated_at для контроля политики смены пароля

Пример расширенной схемы:

ALT ER   TABLE users ADD COLUMN password_updated_at DATETIME;

Обработка ошибок и консистентность данных

При работе с базами данных важна защита от частично выполненных операций. Особенно это актуально при регистрации:

async function safeCreateUser(email, password) {
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();

        const hash = passwordHash.generate(password);

        await conn.execute(
            'INS ERT IN TO users (email, password_hash) VALUES (?, ?)',
            [email, hash]
        );

        await conn.commit();
    } catch (e) {
        await conn.rollback();
        throw e;
    } finally {
        conn.release();
    }
}

В SQLite аналогичный эффект достигается через транзакции:

db.run('BEGIN TRANSACTION');

Хранение и производительность

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

  • индексация применяется только к email или username
  • поле password_hash никогда не индексируется
  • сравнение выполняется только в памяти приложения

Типичные ошибки интеграции

Часто встречающиеся проблемы:

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

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