Реализация переноса пользователей и паролей при миграции сайта
Миграция пользователей — технически сложный процесс из-за несовместимости алгоритмов хэширования паролей между разными платформами. Правильный подход сохраняет пользователей от необходимости сбрасывать пароли.
Проблема несовместимости хэшей
| Платформа | Алгоритм | Формат |
|---|---|---|
| WordPress | phpass (md5-based) | $P$BHash... |
| Drupal 7 | sha512 + соль | $S$5Hash... |
| Laravel | bcrypt | $2y$10$Hash... |
| Django | PBKDF2 SHA256 | pbkdf2_sha256$N$salt$hash |
| PHP legacy | MD5 | 32 символа hex |
| bcrypt | bcrypt | $2a$10$Hash... |
Стратегия 1: Lazy migration (предпочтительно)
Хэши переносятся как есть. При первом входе пользователя проверяется старый алгоритм, при успехе хэш перехэшируется новым алгоритмом.
# 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)
Обработка входа:
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
Проверка совместимости phpass (WordPress)
# Реализация 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)
# Старый MD5 WordPress без соли
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: перенос пользователей
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'],
})
migrated += 1
print(f"Migrated: {migrated}, Skipped: {skipped}")
def detect_wp_hash_algorithm(hash_val):
if hash_val.startswith('$P$') or hash_val.startswith('$H$'):
return 'phpass'
if hash_val.startswith('$2y$') or hash_val.startswith('$2a$'):
return 'bcrypt'
if len(hash_val) == 32:
return 'legacy_md5'
return 'unknown'
Принудительный сброс паролей для старых алгоритмов
Если поддержка legacy алгоритмов нежелательна — уведомить пользователей о сбросе:
def send_password_reset_for_legacy_users():
users = db.query(
"SELECT * FROM users WHERE password_algorithm IN ('legacy_md5', 'sha1')"
)
for user in users:
token = generate_secure_token()
db.save_reset_token(user.id, token, expires_in=7*24*3600)
send_email(
to=user.email,
subject="Необходимо обновить пароль",
template="password_reset_migration",
vars={
'name': user.display_name,
'reset_url': f"https://site.com/reset?token={token}",
'deadline': '7 дней'
}
)
print(f"Sent reset emails to {len(users)} users")
SSO как альтернатива
Если платформы работают одновременно — настроить SSO через OAuth2/SAML:
- Старая платформа выступает OAuth2 Provider
- Новая использует её для аутентификации
- После полной миграции — отключить SSO
Роли и разрешения
ROLE_MAP = {
# WordPress → Custom CMS
'administrator': 'admin',
'editor': 'editor',
'author': 'author',
'contributor': 'contributor',
'subscriber': 'user',
}
def migrate_user_roles(wp_db, new_db):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT user_id, meta_value as capabilities
FROM wp_usermeta WHERE meta_key = 'wp_capabilities'
""")
for row in cursor.fetchall():
caps = php_unserialize(row['capabilities'])
wp_role = list(caps.keys())[0] if caps else 'subscriber'
new_role = ROLE_MAP.get(wp_role, 'user')
new_db.update_user_role(row['user_id'], new_role)
Срок выполнения
Миграция пользователей с lazy password migration и маппингом ролей — 2–3 рабочих дня.







