Стратегия ленивой миграции при входе пользователя

Суть ленивой миграции паролей

Ленивая миграция (lazy migration) — это стратегия постепенного обновления хешей паролей пользователей без принудительного сброса паролей и без массовой фоновой переработки базы данных. Обновление происходит только в момент успешной аутентификации пользователя.

Основная идея заключается в том, что устаревшие алгоритмы хеширования заменяются на более современные по мере естественных входов пользователей в систему. Это позволяет одновременно поддерживать совместимость со старыми данными и повышать криптографическую стойкость без дорогостоящих миграционных операций.

Ключевой принцип:

если пользователь успешно вошёл в систему с устаревшим хешем — пароль пересчитывается и сохраняется в новом формате


Когда применяется стратегия

Ленивая миграция используется в следующих ситуациях:

  • система обновляет алгоритм хеширования (например, переход с SHA-256 на bcrypt или Argon2);
  • меняются параметры безопасности (увеличение cost-фактора);
  • требуется устранить использование устаревших или небезопасных схем хранения паролей;
  • база пользователей слишком велика для единовременной миграции.

Архитектура проверки пароля с поддержкой миграции

Типичный поток аутентификации включает несколько этапов:

  1. Получение пользователя по логину или email.
  2. Определение формата хеша.
  3. Проверка пароля с использованием соответствующего алгоритма.
  4. При успешной проверке — при необходимости обновление хеша.

В контексте библиотеки password-hash логика обычно строится вокруг функции проверки и функции генерации нового хеша.

Пример упрощённой схемы:

const passwordHash = require('password-hash');

function authenticate(user, plainPassword) {
  const storedHash = user.passwordHash;

  const isValid = passwordHash.verify(plainPassword, storedHash);

  if (!isValid) {
    return { success: false };
  }

  if (passwordHash.isDeprecated(storedHash)) {
    const newHash = passwordHash.generate(plainPassword);
    updateUserPasswordHash(user.id, newHash);
  }

  return { success: true };
}

Определение устаревшего хеша

Ключевым элементом ленивой миграции является механизм определения необходимости обновления хеша.

Устаревание может определяться по нескольким критериям:

1. Тип алгоритма

Если хеш создан с использованием алгоритма, который признан небезопасным или устаревшим.

2. Параметры вычисления

Например, слишком низкий cost-фактор у bcrypt-подобных схем.

3. Формат строки хеша

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

Пример логики:

function isDeprecated(hash) {
  if (hash.startsWith('sha256$')) return true;
  if (hash.includes('rounds=5')) return true;
  return false;
}

Обновление хеша в момент входа

Обновление должно выполняться только после успешной проверки пароля. Это критически важно: нельзя менять хеш до подтверждения корректности пароля.

Процесс выглядит следующим образом:

  • пользователь вводит пароль;

  • система проверяет пароль старым методом;

  • если пароль верный:

    • генерируется новый хеш;
    • сохраняется в базу данных;
  • пользователь получает доступ.


Взаимодействие с библиотекой password-hash

Библиотека password-hash обычно предоставляет три ключевых операции:

  • создание хеша;
  • проверка хеша;
  • иногда — указание типа алгоритма.

При реализации ленивой миграции важно учитывать, что библиотека может не хранить метаданные о “силе” хеша, поэтому дополнительная логика должна быть вынесена на уровень приложения.

Пример генерации:

const hash = passwordHash.generate('user-password');

Пример проверки:

const valid = passwordHash.verify('user-password', hash);

Управление несколькими поколениями хешей

В реальных системах часто присутствует сразу несколько поколений хешей:

  • старые SHA-based хеши;
  • промежуточные bcrypt-хеши с низким cost;
  • современные bcrypt/argon2 с усиленными параметрами.

Для поддержки такой модели вводится слой абстракции:

function verifyPassword(password, hash) {
  if (hash.startsWith('sha')) {
    return legacyShaVerify(password, hash);
  }

  return passwordHash.verify(password, hash);
}

И дополнительная проверка на необходимость обновления:

function needsUpgrade(hash) {
  if (hash.startsWith('sha')) return true;
  if (hash.includes('rounds=10')) return true;
  return false;
}

Безопасность при ленивой миграции

Несмотря на удобство, стратегия требует строгого соблюдения правил безопасности:

  • обновление хеша выполняется только при успешной аутентификации;
  • нельзя логировать пароль или промежуточные значения;
  • операция обновления должна быть атомарной;
  • при сбое обновления система не должна откатывать успешный логин;
  • необходимо учитывать конкурентные запросы (например, двойной логин с разных устройств).

Работа с конкурентностью

При одновременной аутентификации одного пользователя возможна ситуация гонки обновления хеша. Для предотвращения проблем применяются:

  • транзакции на уровне базы данных;
  • optimistic locking (версионирование записи пользователя);
  • проверка актуальности хеша перед записью.

Пример защиты:

async function updateHashIfUnchanged(userId, oldHash, newHash) {
  const user = await getUser(userId);

  if (user.passwordHash !== oldHash) {
    return;
  }

  await saveNewHash(userId, newHash);
}

Переход на новый алгоритм без простоя системы

Ленивая миграция позволяет внедрять новые алгоритмы хеширования без массового пересчёта базы:

  • старые пользователи продолжают использовать старый хеш;
  • новые пользователи получают новый хеш автоматически;
  • при входе старых пользователей происходит поэтапное обновление.

Таким образом, система естественным образом “перетекает” на новый стандарт безопасности.


Ограничения подхода

Несмотря на эффективность, стратегия имеет особенности:

  • полная миграция может занять длительное время при низкой активности пользователей;
  • часть пользователей может никогда не обновить хеш;
  • требуется поддержка нескольких алгоритмов одновременно;
  • усложняется логика аутентификации.

Типовая структура сервиса аутентификации с ленивой миграцией

class AuthService {
  async login(email, password) {
    const user = await findUserByEmail(email);

    if (!user) return false;

    const valid = verifyPassword(password, user.passwordHash);
    if (!valid) return false;

    if (needsUpgrade(user.passwordHash)) {
      const newHash = passwordHash.generate(password);
      await updateUserPasswordHash(user.id, newHash);
    }

    return true;
  }
}

Практическая роль в современных системах

Ленивая миграция стала стандартным подходом в системах с большим количеством пользователей. Она используется в:

  • корпоративных платформах;
  • SaaS-сервисах;
  • социальных сетях;
  • банковских и финтех-приложениях.

Её ценность заключается в том, что безопасность повышается постепенно, без влияния на пользовательский опыт и без массовых операций над базой данных.