JS Tower
Node.jsМодуль 6: Безопасность

Хеширование паролей

bcrypt, argon2, salt

Цель урока

В этом уроке ты научишься:

  • Понимать зачем хешировать пароли
  • Использовать bcrypt
  • Работать с argon2

Зачем хешировать

Никогда не храни пароли в открытом виде!

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

Хеширование — односторонняя функция: из пароля можно получить хеш, но из хеша нельзя получить пароль.


bcrypt

Установка

npm install bcrypt

Хеширование

const bcrypt = require('bcrypt');

// Хеширование
const password = 'mypassword123';
const saltRounds = 10;

const hash = await bcrypt.hash(password, saltRounds);
// $2b$10$N9qo8uLOickgx2ZMRZoMy...

// Проверка
const isMatch = await bcrypt.compare('mypassword123', hash);
// true

В модели Mongoose

const userSchema = new mongoose.Schema({
  email: String,
  password: {
    type: String,
    required: true,
    select: false  // Не возвращать по умолчанию
  }
});

// Хешируем перед сохранением
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Метод для проверки пароля
userSchema.methods.comparePassword = async function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

const User = mongoose.model('User', userSchema);

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

// Регистрация
app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.create({ email, password });
  // Пароль автоматически хешируется
  
  res.status(201).json({ id: user._id, email: user.email });
});

// Логин
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email }).select('+password');
  
  if (!user || !await user.comparePassword(password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Генерация токена...
  res.json({ token: '...' });
});

argon2 (рекомендуется)

Более современный и безопасный алгоритм.

Установка

npm install argon2

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

const argon2 = require('argon2');

// Хеширование
const hash = await argon2.hash(password, {
  type: argon2.argon2id,  // Рекомендуемый тип
  memoryCost: 2 ** 16,    // 64MB
  timeCost: 3,
  parallelism: 1
});

// Проверка
const isMatch = await argon2.verify(hash, password);

В модели

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  this.password = await argon2.hash(this.password);
  next();
});

userSchema.methods.comparePassword = async function(candidatePassword) {
  return argon2.verify(this.password, candidatePassword);
};

Сравнение

Критерийbcryptargon2
БезопасностьВысокаяОчень высокая
СкоростьСредняяНастраиваемая
ПамятьФиксированнаяНастраиваемая
СтандартШироко используетсяПобедитель PHC

Salt

Salt — случайные данные, добавляемые к паролю перед хешированием.

// bcrypt автоматически генерирует salt
const hash = await bcrypt.hash(password, 10);
// 10 — количество раундов (cost factor)

// Salt встроен в хеш
// $2b$10$N9qo8uLOickgx2ZMRZoMy...
// $2b$ — версия
// 10$ — cost factor
// N9qo8uLOickgx2ZMRZoMy — salt (22 символа)
// ... — сам хеш

Практика

Задание: Смена пароля

Задача: Реализуй endpoint для смены пароля.

Решение:

app.post('/change-password', authenticate, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  
  const user = await User.findById(req.user.id).select('+password');
  
  // Проверяем текущий пароль
  if (!await user.comparePassword(currentPassword)) {
    return res.status(400).json({ error: 'Current password is incorrect' });
  }
  
  // Устанавливаем новый пароль
  user.password = newPassword;
  await user.save();  // Хеширование в pre-save hook
  
  res.json({ message: 'Password changed' });
});

Проверь себя

  1. Почему нельзя хранить пароли в открытом виде?
  2. Что такое salt?
  3. Чем argon2 лучше bcrypt?