Koa: обработка паролей в контексте контекста запроса

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

bcrypt.js — чистая JavaScript-реализация алгоритма bcrypt, не требующая нативных зависимостей. Это особенно важно для окружений, где невозможна сборка бинарных модулей.

Ключевые характеристики:

  • адаптивная сложность через cost factor
  • встроенная соль (salt), генерируемая автоматически
  • устойчивость к радужным таблицам
  • асимптотически дорогая операция сравнения

Cost factor задаёт количество итераций алгоритма:

2^{cost}

Рост значения cost экспоненциально увеличивает время вычисления хэша.


Установка и подключение

В проектах Node.js bcrypt.js устанавливается как обычная зависимость:

npm install bcryptjs

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

import bcrypt from 'bcryptjs'

или CommonJS:

const bcrypt = require('bcryptjs')

Базовые операции хэширования

Генерация хэша пароля

const password = 'user_password'
const saltRounds = 10

const hash = await bcrypt.hash(password, saltRounds)

hash содержит и соль, и результат хэширования. Это важная особенность bcrypt: отдельное хранение salt не требуется.


Проверка пароля

const isMatch = await bcrypt.compare(password, hashFromDatabase)

Функция compare сама извлекает соль из хэша и выполняет повторное вычисление.


Контекст Koa и обработка паролей

Koa использует объект контекста ctx, который объединяет запрос и ответ в единую абстракцию. Работа с паролями в таком контексте строится вокруг middleware, где ctx.request.body содержит входные данные, а ctx.state используется для передачи данных между слоями.


Регистрация пользователя: хэширование в middleware

Типичный поток регистрации включает преобразование пароля до сохранения в базу данных.

import bcrypt from 'bcryptjs'

async function register(ctx) {
  const { username, password } = ctx.request.body

  const saltRounds = 12
  const passwordHash = await bcrypt.hash(password, saltRounds)

  const user = {
    username,
    password: passwordHash
  }

  // имитация сохранения в БД
  ctx.body = { status: 'ok', user }
}

Ключевой момент — исходный пароль никогда не сохраняется.


Авторизация пользователя: проверка в контексте запроса

async function login(ctx) {
  const { username, password } = ctx.request.body

  const user = await getUserByUsername(username)

  if (!user) {
    ctx.status = 401
    return
  }

  const valid = await bcrypt.compare(password, user.password)

  if (!valid) {
    ctx.status = 401
    return
  }

  ctx.state.user = user
  ctx.body = { status: 'authenticated' }
}

ctx.state.user используется для передачи авторизованного пользователя дальше по цепочке middleware.


Middleware-архитектура Koa для обработки паролей

Koa позволяет разделять ответственность на уровни:

async function authMiddleware(ctx, next) {
  const { password, username } = ctx.request.body

  ctx.state.credentials = { username, password }

  await next()
}

Далее слой сервиса работает уже с нормализованными данными:

async function authService(ctx) {
  const { username, password } = ctx.state.credentials

  const user = await getUser(username)
  const match = await bcrypt.compare(password, user.password)

  ctx.state.user = match ? user : null
}

Соль и устойчивость к атакам

bcrypt автоматически генерирует соль и включает её в итоговый хэш:

$2a$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36...

Структура включает:

  • версию алгоритма
  • cost factor
  • salt
  • hash

Это исключает необходимость ручного управления солью.


Производительность и cost factor

Увеличение cost factor значительно влияет на время ответа сервера:

T 2^{cost}

Практически используемые значения:

  • 8–10: высокая скорость, слабая защита
  • 10–12: баланс безопасности и производительности
  • 12+: высокая безопасность, высокая нагрузка

Обработка ошибок в Koa

bcrypt может выбрасывать ошибки при некорректных входных данных:

try {
  const hash = await bcrypt.hash(password, 12)
} catch (err) {
  ctx.throw(500, 'Hashing error')
}

В Koa предпочтительно использовать ctx.throw для централизованной обработки.


Интеграция с моделью пользователя

При работе с базой данных пароль заменяется хэшем:

const userSchema = {
  username: String,
  passwordHash: String
}

Сравнение всегда происходит через bcrypt.compare, а не через прямое сравнение строк.


Типичные ошибки при работе с bcrypt в Koa

  • хранение plain text паролей в ctx.state
  • повторное хэширование уже захэшированного пароля
  • синхронное использование bcrypt.hashSync в асинхронном сервере
  • игнорирование cost factor и перегрузка сервера

Синхронный и асинхронный режим

bcrypt.js поддерживает оба варианта, но в Koa используется только асинхронный подход:

const hash = bcrypt.hashSync(password, 10)

Синхронные операции блокируют event loop и ухудшают масштабируемость.


Безопасное проектирование слоя авторизации

Контекст Koa позволяет централизовать доступ к данным пользователя:

app.use(async (ctx, next) => {
  ctx.state.auth = {
    isAuthenticated: false,
    user: null
  }

  await next()
})

После успешной проверки:

ctx.state.auth.isAuthenticated = true
ctx.state.auth.user = user

Поведение bcrypt при сравнениях

bcrypt использует защиту от timing attacks за счёт равномерного времени выполнения операции сравнения, независимо от совпадения строки.


Структурирование логики в сервисах

Разделение Koa-контекста и бизнес-логики повышает читаемость:

class AuthService {
  async hashPassword(password) {
    return bcrypt.hash(password, 12)
  }

  async verify(password, hash) {
    return bcrypt.compare(password, hash)
  }
}

Контекст ctx остаётся только транспортным слоем, не содержащим бизнес-логики.


Работа с refresh/login flow

При расширенной авторизации bcrypt используется только на этапе проверки пароля, после чего система обычно переключается на токены (например, JWT), а ctx.state служит временным хранилищем состояния запроса.