Implementation with .NET Keyed Servicesدر NET 8.، مایکروسافت قابلیتی به نام Keyed Services معرفی کرد که برای این سناریو کاملاً ایدهآل است. این قابلیت به ما اجازه میدهد چند پیادهسازی از یک interface یکسان را ثبت کنیم و بر اساس نام (کلید) آنها را دریافت کنیم. 🔑
پیادهسازی با استفاده از Keyed Services در NET. 🧩
1️⃣ Registering the Services
ما هر دو hasher را در فایل Program.cs ثبت میکنیم و به هرکدام یک کلید منحصربهفرد میدهیم:
// Register the implementations with specific keys
builder.Services.AddKeyedSingleton<IPasswordHasher, Pbdkf2PasswordHasher>("legacy");
builder.Services.AddKeyedSingleton<IPasswordHasher, Argon2PasswordHasher>("modern");
// (Optional) Register the modern one as the default for other services
builder.Services.AddSingleton<IPasswordHasher, Argon2PasswordHasher>();
2️⃣ The Login Command Handler
حالا منطق مهاجرت را پیادهسازی میکنیم. هر دو hasher را با استفاده از اتریبیوت [FromKeyedServices] تزریق میکنیم. 🧪
public class LoginCommandHandler(
IUserRepository userRepository,
[FromKeyedServices("modern")] IPasswordHasher newHasher,
[FromKeyedServices("legacy")] IPasswordHasher legacyHasher)
{
public async Task<AuthenticationResult> Handle(LoginCommand command)
{
var user = await userRepository.GetByEmailAsync(command.Email);
if (user is null)
{
return AuthenticationResult.Fail();
}
// 1. Try the new algorithm first (Happy Path)
if (newHasher.Verify(user.PasswordHash, command.Password))
{
return AuthenticationResult.Success(user);
}
// 2. Fallback: Check if it's a legacy hash
if (legacyHasher.Verify(user.PasswordHash, command.Password))
{
// 3. MIGRATION STEP: Re-hash and save
var newHash = newHasher.Hash(command.Password);
user.UpdatePasswordHash(newHash);
await userRepository.SaveChangesAsync();
return AuthenticationResult.Success(user);
}
return AuthenticationResult.Fail();
}
}
این کد تضمین میکند که کاربران فعال به صورت خودکار ارتقا پیدا کنند.
بعد از چند ماه، بخش عمدهای از کاربران شما روی الگوریتم جدید خواهند بود. 📈
Real-World Improvements
بهبودهای دنیای واقعی 🌍
در حالی که پیادهسازی بالا کار میکند، دو بهبود وجود دارد که آن را production-ready میکند.
1️⃣ Algorithm Prefixes
پیشوند الگوریتمها
اتکا به روش «trial and error» برای verify کردن کار میکند، اما تمیزتر این است که دقیقاً بدانیم هر هش با چه الگوریتمی ساخته شده است.
الگوریتمهای استاندارد معمولاً یک prefix دارند
(مثلاً Bcrypt با $2a$ یا $2b$ شروع میشود).
میتوان از این موضوع برای مسیریابی بهینه استفاده کرد:
public bool IsLegacyHash(string hash)
{
// This assumes we're storing a prefix for PBKDF2 hashes. Something to consider.
return hash.StartsWith("pbkdf2$");
}
مزیت دیگر این کار این است که میتوانیم مستقیماً از دیتابیس کوئری بگیریم و کاربرانی که هنوز روی فرمت قدیمی هستند را پیدا کنیم. 🔍
2️⃣ Feature Flags
فلگهای ویژگی 🚩
انجام یک write در دیتابیس هنگام لاگین باعث افزایش latency میشود. اگر ترافیک بالایی دارید، بهتر است کنترل این rollout را در دست بگیرید.
با قرار دادن منطق مهاجرت پشت یک Feature Flag، میتوانید در صورت بالا رفتن فشار روی دیتابیس، مرحلهی نوشتن را غیرفعال کنید، در حالی که کاربران همچنان از مسیر fallback میتوانند لاگین کنند.
if (await featureManager.IsEnabledAsync(FeatureFlags.MigratePasswords) &&
legacyHasher.Verify(user.PasswordHash, command.Password))
{
// Perform migration...
}
Finishing the Migration
پایان دادن به مهاجرت 🏁
بعد از مدتی (معمولاً چند ماه)، اکثر اکانتهای فعال ارتقا پیدا میکنند.
سپس میتوانید یک اسکریپت cleanup اجرا کنید تا هشهای قدیمی باقیمانده را شناسایی کنید و کاربران مربوطه را مجبور کنید در لاگین بعدی پسورد خود را ریست کنند.
در این نقطه میتوانید موارد زیر را حذف کنید:
• ثبت legacy hasher
• مسیر verification مربوط به legacy
• ءfeature flag
و مهاجرت کامل میشود. ✅