Использование хеширования паролей в связке с реляционными базами данных требует чёткого разделения ответственности: библиотека формирует криптографически устойчивое представление пароля, база данных отвечает только за хранение результата и метаданных проверки.
Библиотека password-hash в Node.js применяется для
создания хешей и последующей верификации без необходимости прямого
управления солью и алгоритмом на уровне приложения. При интеграции с
MySQL и SQLite ключевым становится правильное проектирование схемы
хранения и контроль процесса аутентификации.
При регистрации пользователя пароль не сохраняется в открытом виде. Вместо этого используется преобразование:
const passwordHash = require('password-hash');
const hashedPassword = passwordHash.generate('user_password');
Результат содержит алгоритм, соль и сам хеш в одной строке. Это упрощает хранение и исключает необходимость отдельного управления солью.
Проверка выполняется следующим образом:
const isValid = passwordHash.verify('user_password', hashedPassword);
Независимо от СУБД, минимальная схема включает идентификатор пользователя и поле для хранения хеша.
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
);
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, поддерживающая промисы.
Установка:
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]
);
}
Процесс аутентификации строится на выборке хеша и его проверке:
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 часто используется в локальных приложениях, прототипах и
десктопных решениях. На практике применяются 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]
);
}
Так как 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);
}
);
}
Во всех сценариях обязательным является применение параметризованных запросов. Это исключает SQL-инъекции на этапе передачи email и других пользовательских данных.
Пример корректного подхода:
'SEL ECT password_hash FR OM users WHERE email = ?'
Недопустимо формировать SQL через конкатенацию строк:
// небезопасно
"SEL ECT * FR OM users WHERE email = '" + email + "'"
При смене пароля выполняется перегенерация хеша:
async function updatePassword(email, newPassword) {
const hash = passwordHash.generate(newPassword);
await pool.execute(
'UPD ATE users SE T password_hash = ? WHERE email = ?',
[hash, email]
);
}
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 вместо
VARCHARpassword_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 или
usernamepassword_hash никогда не индексируетсяЧасто встречающиеся проблемы:
Корректная архитектура предполагает, что база данных не знает ничего о процессе хеширования и рассматривает хеш как обычную строку фиксированного назначения.