Перенос пользователей — технический квест, который мы решаем без потери доступа
Отметим: когда бизнес решает переехать на новую CMS или фреймворк, самый деликатный вопрос — пользователи. Их пароли зашифрованы старым алгоритмом — sha512 с солью (Drupal 7), phpass (WordPress) или md5($password.$salt) из самописной CRM. Новая система эти хэши не понимает, и просто скопировать базу нельзя — никто не войдёт. Более того, инженеры часто допускают ошибку: копируют таблицу пользователей как есть, а потом выясняют, что все пароли недействительны. Результат — массовый сброс сессий и потеря лояльности. Мы провели более 50 миграций, включая проекты с 200 000 пользователей, и знаем, как провести перенос так, чтобы пользователи даже не заметили переезда. Ниже — проверенная стратегия и инструменты (Python, Golang, Laravel, Django), которые мы используем в каждом проекте. Мы гарантируем, что ни один пользователь не потеряет доступ.
Какие проблемы решаем?
- Несовместимость алгоритмов хэширования: старая система использует
sha512илиMD5, новая — толькоbcrypt. - Дубликаты пользователей по email при объединении баз.
- Потеря ролей и прав доступа при переносе.
- Необходимость сохранить активные сессии и избежать массового перелогина.
- SSO-интеграция для временной совместной работы старой и новой платформ.
Почему lazy migration лучше полного рехэширования?
Lazy migration позволяет перенести пользователей без простоя и потери доступа. Старые хэши остаются в базе, а при каждом входе проверяется алгоритм и при успехе пароль перехэшируется новым. Это в 3–5 раз быстрее полного принудительного сброса и не вызывает отток аудитории. Кроме того, он позволяет постепенно переносить пользователей без простоя — можно мигрировать базу в фоне, а 100% нагрузка на старую систему сохраняется.
| Стратегия | Время реализации | Влияние на пользователей | Безопасность |
|---|---|---|---|
| Lazy migration | 1-2 дня | Минимальное, процесс прозрачен | Высокая при правильной реализации |
| Принудительный сброс | 1-3 дня | Требует от пользователя смены пароля | Высокая |
| Полное рехэширование | 3-7 дней | Среднее, возможна временная недоступность | Средняя (ошибки при массовом перехэшировании) |
Принудительный сброс для legacy алгоритмов
Если поддержка устаревших алгоритмов (MD5, SHA1) нежелательна, мы отправляем пользователям email с токеном для сброса. Это безопаснее, чем хранить ненадёжные хэши. Сценарий: скрипт находит всех пользователей с алгоритмом legacy_md5, генерирует одноразовый токен и отправляет письмо. Срок действия токена — 7 дней. После успешной смены пароль сохраняется уже с bcrypt.
Что делать, если алгоритм не поддерживается новой системой?
В этом случае мы внедряем промежуточный слой верификации. Например, для phpass (WordPress) используем собственную реализацию на Python. «PHPass is a portable public domain password hashing framework» — его алгоритм поддерживается через кастомную функцию. Аналогично для PBKDF2 (Django) и sha512 (Drupal 7). Это позволяет сохранить совместимость без переписывания всей системы.
Как реализовать lazy migration?
Пошаговый план:
- Проанализируйте существующие хэши и определите алгоритмы.
- Реализуйте функцию верификации для каждого алгоритма.
- Добавьте логику перехэширования при успешном входе.
- Разработайте ETL-скрипт для переноса данных.
- Протестируйте на копии базы.
- Запустите миграцию и мониторьте ошибки.
Основная идея — хранить исходный хэш и метку алгоритма в поле password_algorithm. При входе вызываем функцию verify_password, которая проверяет алгоритм, и при успехе — upgrade_password_hash. Рассмотрим реализацию на Python.
# models/user.py
class User(BaseModel):
password_hash: str
password_algorithm: str # 'bcrypt', 'phpass', 'sha512', 'legacy_md5'
def verify_password(self, plain_password: str) -> bool:
if self.password_algorithm == 'bcrypt':
return bcrypt.checkpw(plain_password.encode(), self.password_hash.encode())
elif self.password_algorithm == 'phpass':
return phpass_check(plain_password, self.password_hash)
elif self.password_algorithm == 'legacy_md5':
return hashlib.md5(plain_password.encode()).hexdigest() == self.password_hash
elif self.password_algorithm == 'pbkdf2_sha256':
return django_pbkdf2_check(plain_password, self.password_hash)
return False
def upgrade_password_hash(self, plain_password: str):
new_hash = bcrypt.hashpw(plain_password.encode(), bcrypt.gensalt(rounds=12))
self.password_hash = new_hash.decode()
self.password_algorithm = 'bcrypt'
db.save(self)
Проверка совместимости phpass (WordPress)
Детали реализации для WordPress
WordPress использует [phpass](https://en.wikipedia.org/wiki/PHPass). Для интеграции с Python:import hashlib
ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
def phpass_check(password: str, stored_hash: str) -> bool:
if stored_hash.startswith('$P$') or stored_hash.startswith('$H$'):
return _phpass_verify(password, stored_hash)
return hashlib.md5(password.encode()).hexdigest() == stored_hash
def _phpass_verify(password: str, hash_str: str) -> bool:
count_log2 = ITOA64.index(hash_str[3])
count = 1 << count_log2
salt = hash_str[4:12]
hash_val = hashlib.md5((salt + password).encode()).digest()
for _ in range(count):
hash_val = hashlib.md5(hash_val + password.encode()).digest()
output = _encode64(hash_val, 16)
return hash_str[12:34] == output[:22]
ETL: перенос пользователей с маппингом ролей
Для массового переноса используем ETL-скрипт. Пример для WordPress:
def migrate_users_from_wordpress(wp_db, new_db):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT u.ID, u.user_login, u.user_pass, u.user_email,
u.user_registered, u.display_name,
um.meta_value as first_name, um2.meta_value as last_name
FROM wp_users u
LEFT JOIN wp_usermeta um ON u.ID = um.user_id AND um.meta_key = 'first_name'
LEFT JOIN wp_usermeta um2 ON u.ID = um2.user_id AND um2.meta_key = 'last_name'
ORDER BY u.ID
""")
migrated = 0
skipped = 0
for wp_user in cursor.fetchall():
existing = new_db.get_user_by_email(wp_user['user_email'])
if existing:
skipped += 1
continue
algorithm = detect_wp_hash_algorithm(wp_user['user_pass'])
new_db.create_user({
'username': wp_user['user_login'],
'email': wp_user['user_email'],
'password_hash': wp_user['user_pass'],
'password_algorithm': algorithm,
'display_name': wp_user['display_name'],
'created_at': wp_user['user_registered'],
'legacy_id': wp_user['ID'],
})
# Маппинг ролей: из wp_usermeta meta_key = 'wp_capabilities'
roles = get_user_roles(wp_db, wp_user['ID'])
new_db.assign_roles(wp_user['user_email'], roles)
migrated += 1
print(f"Migrated: {migrated}, Skipped: {skipped}")
Обработка входа и перехэширование
def login(email: str, password: str):
user = db.get_user_by_email(email)
if not user:
return None
if user.verify_password(password):
if user.password_algorithm != 'bcrypt':
user.upgrade_password_hash(password)
return create_session(user)
return None
Что входит в работу по миграции пользователей?
- Аудит текущей базы пользователей и алгоритмов хэширования.
- Разработка скриптов ETL с учётом маппинга ролей и дополнительных полей.
- Реализация lazy migration (или принудительного сброса) с тестированием на копии.
- Настройка мониторинга ошибок после деплоя (24-часовое наблюдение).
- Документация по процедуре отката и резервное копирование.
Сроки выполнения и типичные ошибки
Ориентировочные сроки
- Для базы до 100 000 пользователей: 2–3 рабочих дня на аудит, разработку и тестирование.
- Для базы от 100 000 до 500 000: 5–7 дней с учётом ETL и нагрузочного тестирования.
- В крупных проектах с десятками ролей и кастомными полями срок может быть увеличен.
Типичные ошибки при миграции
- Пропуск проверки дубликатов по email — приводит к конфликтам и потере данных.
- Игнорирование маппинга ролей — пользователь переносится, но теряет права доступа.
- Попытка рехэшировать все пароли сразу — вызывает CPU-нагрузку и ошибки.
- Отсутствие плана отката — при неудаче нет быстрого восстановления.
- Незакрытие старых сессий — пользователи остаются в системе под старыми данными.
Советы:
- Всегда тестируйте на копии базы.
- Используйте транзакции и скрипты с rollback.
- Настройте мониторинг ошибок после деплоя (первые 24 часа критичны).
- Храните резервную копию старой базы и скрипт обратной миграции.
Сравнение алгоритмов хэширования
| Алгоритм | Длина хэша | Устойчивость к брутфорсу | Стандарт в CMS |
|---|---|---|---|
| bcrypt | 60 символов | Высокая (cost adjustable) | Laravel, Symfony, Rails |
| phpass | 34 символа | Средняя (MD5-based) | WordPress, Drupal 7 |
| PBKDF2 | 98 символов | Высокая | Django, Python |
| MD5 | 32 символа | Низкая | Устаревшие системы |
Закажите аудит вашего проекта — первый день бесплатно. Свяжитесь с нами для консультации и получите точную смету с гарантией результата.







