Перейти к основному содержанию
Разработка3 мин чтения

Аутентификация в веб-приложении: как сделать правильно

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

Латунный ключ и связка отпечатков рядом с экраном входа

Аутентификацию почти всегда недооценивают в оценке проекта. Форма входа рисуется за час, а корректная работа с сессиями, восстановлением пароля и защитой от перебора занимает несколько дней. Разберём, из чего складывается эта работа.

Первое правило: не изобретайте хеширование

Пароли хранятся только в виде хеша, созданного алгоритмом, специально предназначенным для паролей: bcrypt, scrypt или argon2. Быстрые хеш-функции вроде SHA-256 для этого не годятся именно потому, что они быстрые — перебор на видеокарте идёт миллиардами вариантов в секунду.

Неверный и верный способ хранения пароля
import { createHash } from "node:crypto"
import { hash, verify } from "@node-rs/argon2"

// Неверно: быстрая хеш-функция, подбирается перебором
const bad = createHash("sha256").update(password).digest("hex")

// Верно: алгоритм с настраиваемой стоимостью вычисления
const passwordHash = await hash(password, {
  memoryCost: 19456,
  timeCost: 2,
  parallelism: 1,
})

// Проверка при входе
const isValid = await verify(passwordHash, submittedPassword)

Сессии или токены

Спор о сессиях и JWT сводится к одному вопросу: можно ли мгновенно отозвать доступ. Идентификатор сессии в базе отзывается удалением строки. Подписанный токен действует до истечения срока, и отозвать его без дополнительного хранилища нельзя.

Сравнение подходов
КритерийСессия в базеПодписанный токен
Мгновенный отзыв доступаДаНет без списка отзыва
Обращение к базе на запросДа, одно чтениеНе требуется
Смена прав пользователяПрименяется сразуПосле обновления токена
Подходит дляВеб-приложений с входомМежсервисного взаимодействия

Для веб-приложения с личным кабинетом мы почти всегда выбираем сессии в базе: одно чтение по индексу дешевле, чем невозможность выкинуть скомпрометированную сессию.

Сессионная cookie требует четырёх атрибутов. Пропуск любого из них открывает конкретный вектор атаки, поэтому запомнить стоит все.

Корректные атрибуты сессионной cookie
cookies().set("session", sessionId, {
  // Недоступна из JavaScript: защита при межсайтовом скриптинге
  httpOnly: true,
  // Только по HTTPS
  secure: process.env.NODE_ENV === "production",
  // Не отправляется при межсайтовых переходах: защита от CSRF
  sameSite: "lax",
  // Ограничивает область действия
  path: "/",
  // Конечный срок жизни: сессия не живёт вечно
  maxAge: 60 * 60 * 24 * 7,
})

Защита от перебора

Без ограничения частоты попыток форма входа перебирается автоматически. Ограничивать нужно по двум ключам одновременно: по адресу и по логину. Первый останавливает перебор с одной машины, второй — распределённый перебор одной учётной записи.

Ограничение попыток входа
const ipKey = `login:ip:${ip}`
const userKey = `login:user:${email.toLowerCase()}`

const [ipAttempts, userAttempts] = await Promise.all([
  redis.incr(ipKey),
  redis.incr(userKey),
])

// Окно ставим только при первой попытке, чтобы счётчик не продлевался
if (ipAttempts === 1) await redis.expire(ipKey, 900)
if (userAttempts === 1) await redis.expire(userKey, 900)

if (ipAttempts > 20 || userAttempts > 5) {
  return { error: "Слишком много попыток входа. Повторите через 15 минут." }
}

Четыре ошибки, которые встречаются чаще всего

  • Разные сообщения для неверного логина и неверного пароля. Так проверяется существование учётной записи. Сообщение должно быть одно.
  • Сессия не обновляется после входа. Позволяет закрепить заранее известный идентификатор сессии за жертвой.
  • Запрос данных без проверки владельца. Проверка прав в интерфейсе не защищает: ограничение должно быть в самом запросе к базе.
  • Бессрочные ссылки восстановления пароля. Срок жизни — не более часа, и ссылка становится недействительной после первого использования.

Аутентификация — та часть системы, где самостоятельное творчество почти всегда проигрывает проверенной библиотеке.

Частые вопросы

Как правильно хранить пароли пользователей?
Только в виде хеша, полученного алгоритмом, предназначенным для паролей: argon2, scrypt или bcrypt. Быстрые хеш-функции вроде SHA-256 не подходят, поскольку допускают перебор миллиардов вариантов в секунду на видеокарте. В большинстве проектов лучше использовать готовую библиотеку аутентификации.
Что выбрать: сессии в базе или JWT-токены?
Для веб-приложения с личным кабинетом предпочтительны сессии в базе: их можно мгновенно отозвать удалением записи, а изменения прав применяются немедленно. Подписанные токены удобны для межсервисного взаимодействия, но отозвать их до истечения срока без дополнительного хранилища невозможно.
Как защитить форму входа от перебора пароля?
Ограничивать частоту попыток одновременно по двум ключам: по IP-адресу и по логину. Первый останавливает перебор с одной машины, второй — распределённую атаку на конкретную учётную запись. Также важно, чтобы сообщение об ошибке было одинаковым для неверного логина и неверного пароля.
Какие атрибуты обязательны для сессионной cookie?
Четыре: httpOnly закрывает доступ из JavaScript, secure разрешает передачу только по HTTPS, sameSite защищает от межсайтовых запросов, а конечный срок жизни не даёт сессии действовать бессрочно. Пропуск любого из них открывает конкретный вектор атаки.
безопасностьаутентификацияархитектура

Похожая задача в вашем проекте?

Разберём вашу ситуацию и пришлём оценку по этапам — без обязательств и общих слов.

Обсудить задачу

Вопрос по статье
или по своему проекту

Отвечаем на заявки в течение одного рабочего дня. Если задача не наша — скажем прямо и подскажем, к кому обратиться.

Написать нам

Поля со звёздочкой обязательны для заполнения.