Регистрация и хеширование паролей
Пароль нельзя хранить в открытом виде — только хеш с солью через bcrypt или argon2.
«Если базу украдут, открытые пароли — катастрофа. Хеш с солью превращает кражу в бесполезный набор символов.»
Регистрация — первая по-настоящему ответственная часть бэкенда. Здесь нельзя ошибиться: пароли пользователей нужно хранить так, чтобы даже при утечке базы их нельзя было восстановить. В этом уроке разберём хеширование, соль и почему bcrypt лучше простого хеша.
Почему нельзя хранить пароль
Если хранить пароли в открытом виде, любая утечка базы раскрывает их все. Хеширование — односторонняя функция: из пароля легко получить хеш, но из хеша пароль не восстановить. При входе мы хешируем введённый пароль и сравниваем с сохранённым хешем.
Соль против радужных таблиц
Простой хеш уязвим: одинаковые пароли дают одинаковый хеш, и атакующий использует заранее посчитанные таблицы (радужные таблицы). Соль — случайная добавка к каждому паролю — ломает этот приём:
без соли: hash('123456') = a1b2c3 (всегда одинаково -> взлом по таблице)
с солью: hash('123456' + 'x9f2') = z8y7 (у каждого свой)
hash('123456' + 'k1m4') = q3w2 (тот же пароль -- другой хеш)bcrypt делает это автоматически: генерирует соль, встраивает её в результат и намеренно работает медленно, чтобы перебор был дорогим.
Регистрация с bcrypt
const bcrypt = require('bcrypt');
app.post('/register', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'нужны email и password' });
}
// 10 -- 'стоимость': чем больше, тем медленнее и безопаснее
const hash = await bcrypt.hash(password, 10);
const user = await db.user.create({ email, passwordHash: hash });
res.status(201).json({ id: user.id, email: user.email });
// НИКОГДА не возвращаем passwordHash!
});Проверка пароля при входе
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.user.findByEmail(email);
// одинаковый ответ, есть юзер или нет -- не подсказываем атакующему
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'неверные данные' });
}
res.json({ message: 'вход выполнен' });
});Сравнение хешей в браузере
Смоделируем идею хеширования с солью простой (учебной!) функцией. Настоящий bcrypt считает иначе, но принцип "пароль + соль = детерминированный отпечаток" тот же:
// УЧЕБНАЯ хеш-функция (не для реального использования!)
function toyHash(str) {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = (h * 31 + str.charCodeAt(i)) % 1000000;
}
return h;
}
const salt = 'x9f2';
const stored = toyHash('123456' + salt); // сохранили при регистрации
function check(input) {
return toyHash(input + salt) === stored;
}
console.log(check('wrong')); // false
console.log(check('123456')); // trueКак работает под капотом
bcrypt — это не просто хеш, а алгоритм с настраиваемой "стоимостью" (cost factor). Чем выше число, тем больше раундов вычислений и тем медленнее хеширование. Это сделано специально: для одного входа задержка незаметна, но для перебора миллионов вариантов — непреодолима. Соль bcrypt генерирует сам и хранит прямо в строке хеша, поэтому отдельное поле для неё не нужно.
Частые ошибки
- Хранить пароль или его обратимое шифрование. Только односторонний хеш — и только bcrypt/argon2, не md5/sha1.
- Возвращать хеш клиенту. Формируй ответ явно, исключая поле пароля.
- Разные ответы для "нет юзера" и "неверный пароль". Это подсказывает атакующему существование email. Отвечай одинаково.
Best practices
- Используй bcrypt (cost 10-12) или argon2id для хеширования паролей.
- Никогда не логируй и не возвращай пароли и хеши.
- Валидируй силу пароля при регистрации и используй HTTPS.
Итоги
Пароли хранят только как хеш с солью, и bcrypt делает это правильно и медленно — назло переборщикам. Регистрация и вход проверяют пароль через bcrypt.compare. Но после входа нужно как-то помнить, что пользователь авторизован, — этим займёмся в следующем уроке про JWT.
Тайминг и утечки по времени
Тонкий момент безопасности входа — атаки по времени ответа. Если для несуществующего email сервер отвечает мгновенно, а для существующего тратит время на bcrypt.compare, атакующий по задержке угадывает, какие адреса зарегистрированы. Грамотные реализации выравнивают время: сравнивают пароль даже для несуществующего пользователя с фиктивным хешем, чтобы ветви "нет юзера" и "неверный пароль" занимали примерно одинаковое время и давали одинаковый ответ. Это та степень аккуратности, которая отличает учебную форму входа от боевой. Хорошая новость: bcrypt и связка с единообразным 401-ответом уже закрывают большую часть подобных утечек.