احراز هویت بیومتریک در .NET MAUI با کمک پلاگین Plugin.Maui.Biometric یا Plugin.Fingerprint پیاده میشود. این پلاگینها در پشتصحنه به فریمورک LocalAuthentication در iOS و کلاس BiometricPrompt در اندروید متصل میشوند تا با چند خط کد C# بتوانید Face ID، Touch ID و حسگر اثر انگشت را فعال کنید. اصل طلایی این است که بیومتریک هرگز جایگزین رمز عبور نیست؛ بلکه «کلید آنباکس» مقدار حساسی است که قبلاً در SecureStorage ذخیره کردهاید (مثل توکن JWT یا کلید Refresh).
راستش را بخواهید، من خودم در پروژه قبلیام دقیقاً همین قانون را نادیده گرفتم و فکر کردم میشود بیومتریک را بهعنوان «تنها» مکانیزم لاگین بهکار برد. نتیجه؟ App Store اپ را رد کرد و مجبور شدم در آخرین لحظه کل جریان را بازنویسی کنم. پس اگر میخواهید این مسیر را درست بروید، تا انتها همراه باشید.
برای .NET MAUI 9 و 10، پلاگین Plugin.Maui.Biometric رایجترین گزینه است و از Face ID، Touch ID، اثر انگشت و Face Unlock اندروید پشتیبانی میکند.
در iOS باید کلید NSFaceIDUsageDescription را در Info.plist اضافه کنید وگرنه اپلیکیشن هنگام اولین فراخوانی Face ID کرش میکند.
در اندروید مجوز USE_BIOMETRIC (API 28+) و وابستگی androidx.biometric:biometric الزامی است.
الگوی صحیح: کاربر یک بار با رمز عبور لاگین میکند، Refresh Token در SecureStorage ذخیره میشود و دفعات بعد بیومتریک «قفل» این توکن را باز میکند.
همیشه با IsAvailableAsync() در دسترس بودن سختافزار را چک کنید و یک Fallback به PIN یا رمز عبور داشته باشید.
پس از تغییر در ثبتنام بیومتریک کاربر (مثلاً اضافه شدن اثر انگشت جدید)، باید مجدداً اعتبارسنجی با رمز انجام شود. به این کار Biometry Invalidation میگویند.
بیومتریک در .NET MAUI چگونه کار میکند؟
وقتی در .NET MAUI متدی مثل AuthenticateAsync() را فراخوانی میکنید، پلاگین در پشتصحنه با Handlerهای پلتفرم صحبت میکند. در iOS کلاسی به نام LAContext از فریمورک LocalAuthentication ساخته میشود و متد EvaluatePolicy با سیاست DeviceOwnerAuthenticationWithBiometrics اجرا میشود. سیستمعامل خودش UI سیستمی Face ID یا Touch ID را نمایش میدهد و شما هیچ کنترل بصری روی این دیالوگ ندارید. این یک ویژگی امنیتی است، نه باگ.
در اندروید (API 28 به بعد) از androidx.biometric.BiometricPrompt استفاده میشود که یک API یکپارچه برای انواع حسگرها (اثر انگشت، تشخیص چهره، تشخیص عنبیه) فراهم میکند. در نسخههای قبل از API 28، پلاگین به FingerprintManagerCompat برمیگردد، ولی این مسیر منسوخ شده و در .NET MAUI 10 پشتیبانی نمیشود.
کلید درک این موضوع این است که بیومتریک خودش «هویت» را تأیید نمیکند. فقط ثابت میکند که صاحب دستگاه آن لحظه پشت گوشی است. به همین دلیل، الگوی صحیح این است که جریان احراز هویت اپلیکیشن شما (مثلاً احراز هویت JWT با Refresh Token) یک بار با نام کاربری و رمز انجام شود، توکن در SecureStorage پلتفرم ذخیره گردد، و دفعات بعد بیومتریک فقط دسترسی به این توکن را آزاد کند. این روش هم تجربه کاربری روان دارد، هم در صورت سرقت دستگاه بدون انگشت یا چهره مالک، حملهکننده به توکن نمیرسد.
نصب و راهاندازی Plugin.Maui.Biometric
برای پروژههای MAUI 9 و 10، توصیه میکنم از پکیج Plugin.Maui.Biometric استفاده کنید که نگهداری فعالی دارد و APIهای جدید iOS 17 و Android 14 را پشتیبانی میکند. در پروژه خود ابتدا پکیج را نصب کنید.
سپس در فایل MauiProgram.cs سرویس را ثبت کنید تا از طریق DI در سراسر اپ در دسترس باشد. این الگو مشابه ثبت سایر سرویسها در معماری MVVM با CommunityToolkit است.
using Plugin.Maui.Biometric;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// ثبت سرویس بیومتریک بهصورت Singleton
builder.Services.AddSingleton(Biometric.Default);
// سرویس سفارشی ما برای آنباکس توکن
builder.Services.AddSingleton<ISecureSessionService, SecureSessionService>();
return builder.Build();
}
پیکربندی Face ID و Touch ID در iOS
برای iOS باید دو کار انجام دهید: افزودن توضیح به Info.plist و اطمینان از سطح Deployment Target مناسب. اپل از سال ۲۰۱۷ به بعد، اگر اپلیکیشن بدون NSFaceIDUsageDescription سعی کند Face ID را فراخوانی کند، فرآیند را با کرش متوقف میکند. این یکی از شایعترین باگهایی است که توسعهدهندگان MAUI با آن مواجه میشوند. خودم در نسخه اولی که به TestFlight فرستادم، دقیقاً همین کرش را گرفتم و دو ساعت زمان برد تا متوجه شوم Info.plist را فراموش کردهام.
فایل Platforms/iOS/Info.plist را باز کنید و این کلید را اضافه کنید:
<key>NSFaceIDUsageDescription</key>
<string>برای ورود امن و سریع به حساب کاربری، از Face ID استفاده میکنیم.</string>
این متن دقیقاً همان چیزی است که در دیالوگ سیستمی iOS به کاربر نمایش داده میشود. پس باید روشن، دوستانه و توضیحدهنده ارزش این قابلیت باشد. اپل در بازبینی App Store اگر این متن مبهم یا تهدیدآمیز باشد، اپ را رد میکند. برای جزئیات بیشتر میتوانید به مستندات رسمی LocalAuthentication اپل مراجعه کنید.
برای Touch ID نیازی به کلید جداگانه نیست. همان NSFaceIDUsageDescription کافی است و فقط زمانی نمایش داده میشود که دستگاه از Face ID پشتیبانی کند. در دستگاههای Touch ID، iOS دیالوگ استاندارد اثر انگشت را بدون نیاز به مجوز اضافی نمایش میدهد.
پیکربندی BiometricPrompt در اندروید
در پروژه اندروید سه تغییر لازم دارید. اول، در Platforms/Android/AndroidManifest.xml مجوز را اضافه کنید:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- برای پشتیبانی از API های پایینتر از 28 (اختیاری در 2026) -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
دوم، فایل .csproj پروژه را باز کنید و مطمئن شوید که TargetFramework اندروید روی net10.0-android34.0 یا بالاتر است. BiometricPrompt در API 28 معرفی شد ولی متدهای پیشرفته مثل setAllowedAuthenticators از API 30 به بعد در دسترس هستند.
سوم، فعالیت اصلی (MainActivity) باید از ComponentActivity یا AppCompatActivity ارثبری کند تا BiometricPrompt به آن متصل شود. در شابلون پیشفرض MAUI این مورد رعایت شده، ولی اگر تم سفارشی دارید چک کنید. جزئیات کامل API را در مستندات BiometricPrompt اندروید ببینید.
سرویس قابلتست با MVVM و تزریق وابستگی
قبل از اینکه کد را در ViewModel بنویسیم، یک سرویس انتزاعی بسازیم تا منطق بیومتریک از UI جدا بماند و قابل Mock کردن در تستهای واحد باشد. این الگو همان رویکردی است که در تستنویسی .NET MAUI با Appium توضیح دادهایم.
public interface ISecureSessionService
{
Task<bool> IsBiometricAvailableAsync();
Task<BiometricResult> AuthenticateAndUnlockAsync(string reason);
Task SaveTokenAsync(string token);
Task<string?> GetTokenAsync();
Task ClearAsync();
}
public record BiometricResult(bool Success, string? Token, string? ErrorMessage);
پیادهسازی این سرویس از IBiometric پلاگین و SecureStorage داخلی MAUI استفاده میکند:
using Plugin.Maui.Biometric;
public class SecureSessionService : ISecureSessionService
{
private const string TokenKey = "jwt_refresh_token";
private readonly IBiometric _biometric;
public SecureSessionService(IBiometric biometric)
{
_biometric = biometric;
}
public async Task<bool> IsBiometricAvailableAsync()
{
var status = await _biometric.GetAuthenticationStatusAsync(
AuthenticatorStrength.Strong);
return status == BiometricHwStatus.Success;
}
public async Task<BiometricResult> AuthenticateAndUnlockAsync(string reason)
{
var request = new AuthenticationRequest
{
Title = "ورود امن",
Reason = reason,
CancelButtonText = "انصراف",
FallbackButtonText = "استفاده از رمز",
AuthStrength = AuthenticatorStrength.Strong
};
var response = await _biometric.AuthenticateAsync(request);
if (response.Status != BiometricResponseStatus.Success)
return new BiometricResult(false, null, response.ErrorMsg);
var token = await SecureStorage.GetAsync(TokenKey);
return new BiometricResult(true, token, null);
}
public Task SaveTokenAsync(string token) =>
SecureStorage.SetAsync(TokenKey, token);
public async Task<string?> GetTokenAsync() =>
await SecureStorage.GetAsync(TokenKey);
public Task ClearAsync()
{
SecureStorage.Remove(TokenKey);
return Task.CompletedTask;
}
}
توجه کنید که از AuthenticatorStrength.Strong استفاده کردهایم. این پارامتر فقط حسگرهای کلاس ۳ اندروید را قبول میکند (سطح FAR کمتر از ۱ در ۵۰٬۰۰۰) و در iOS فقط Face ID و Touch ID واقعی را میپذیرد، نه روشهای ضعیفتر مثل Pattern Unlock.
آنباکس توکن JWT با بیومتریک
حالا در ViewModel صفحه ورود، میتوانیم جریان زیر را پیاده کنیم: اگر کاربر قبلاً لاگین کرده و توکن در SecureStorage موجود است، با یک تپ روی دکمه «ورود سریع» بیومتریک فعال شود.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class LoginViewModel : ObservableObject
{
private readonly ISecureSessionService _session;
private readonly IAuthApi _authApi;
[ObservableProperty] private bool _isBiometricButtonVisible;
[ObservableProperty] private string? _errorMessage;
public LoginViewModel(ISecureSessionService session, IAuthApi authApi)
{
_session = session;
_authApi = authApi;
}
public async Task OnAppearingAsync()
{
var hasToken = !string.IsNullOrEmpty(await _session.GetTokenAsync());
var hasHardware = await _session.IsBiometricAvailableAsync();
IsBiometricButtonVisible = hasToken && hasHardware;
}
[RelayCommand]
private async Task QuickLoginAsync()
{
var result = await _session.AuthenticateAndUnlockAsync(
"برای ادامه، هویت خود را تأیید کنید");
if (!result.Success || string.IsNullOrEmpty(result.Token))
{
ErrorMessage = result.ErrorMessage ?? "احراز هویت ناموفق بود";
return;
}
var newAccessToken = await _authApi.RefreshAsync(result.Token);
await Shell.Current.GoToAsync("//main");
}
}
استراتژی Fallback و مدیریت خطا
هیچگاه فرض نکنید بیومتریک کار خواهد کرد. کاربر ممکن است دست خیس داشته باشد، در نور کم باشد، عینک آفتابی زده باشد، یا حسگر دستگاه قدیمی و کند باشد. سرویس شما باید برای هر کد خطای پلاگین یک پاسخ مشخص داشته باشد:
کد خطا
معنی
اقدام پیشنهادی
NotEnrolled
کاربر بیومتریکی ثبت نکرده
هدایت به تنظیمات دستگاه
NoHardware
دستگاه فاقد حسگر است
پنهان کردن گزینه و استفاده از رمز
LockedOut
تعداد تلاشهای ناموفق زیاد
درخواست رمز عبور یا PIN
UserCancel
کاربر دیالوگ را بست
عدم نمایش پیام خطا، فقط برگشت
AuthenticationFailed
چهره/انگشت تشخیص داده نشد
اجازه ۲ تلاش مجدد، سپس Fallback
BiometryChanged
ثبتنام بیومتریک تغییر کرده
اجبار به لاگین مجدد با رمز
منطق Fallback را همیشه به صفحه ورود سنتی متصل کنید. کاربری که نتواند با Face ID وارد شود نباید در حلقه بسته گیر کند. در پروژههای واقعی، یک Counter داخلی نگه میداریم و پس از سه شکست متوالی، دکمه بیومتریک را تا ریستارت اپ مخفی میکنیم.
تشخیص تغییر ثبتنام بیومتریک (Invalidation)
یک سناریوی حساس امنیتی این است: کاربر A اپ شما را نصب میکند، با اثر انگشت قفل میگذارد. سپس دستگاه را به کاربر B میدهد. کاربر B اثر انگشت خود را به تنظیمات اضافه میکند. حالا اگر اپ شما هنوز با اثر انگشت B هم باز شود، یک حفره امنیتی جدی دارید.
پلاگین Plugin.Maui.Biometric از iOS 9+ و Android 6+ امکان تشخیص این تغییر را با ذخیره یک biometric state hash میدهد. در زمان لاگین اولیه این هش را ذخیره کنید، و هر بار قبل از باز کردن توکن آن را مقایسه کنید:
public async Task<bool> HasBiometryChangedAsync()
{
var currentState = await _biometric.GetBiometryStateHashAsync();
var storedState = await SecureStorage.GetAsync("biometry_state");
if (string.IsNullOrEmpty(storedState))
{
await SecureStorage.SetAsync("biometry_state", currentState);
return false;
}
return !string.Equals(currentState, storedState, StringComparison.Ordinal);
}
اگر این متد true برگرداند، توکن ذخیرهشده را پاک کنید و کاربر را به صفحه لاگین کامل با نام کاربری و رمز هدایت کنید. این یک الزام Apple Review Guideline 5.1.1 است و در صورت رعایت نکردن، اپ شما در بازبینی رد میشود. منبع رسمی این الزام در مستندات SecureStorage مایکروسافت به آن اشاره شده است.
تست در شبیهساز و دیوایس واقعی
تست بیومتریک یکی از سختترین بخشهای توسعه است چون شبیهسازها چهره و انگشت ندارند. خوشبختانه هر دو پلتفرم ابزارهای شبیهسازی دارند.
iOS Simulator
در Xcode Simulator، از منوی Features → Face ID (یا Touch ID برای دستگاههای قدیمی) میتوانید Enrolled بودن را فعال و سپس Matching Face یا Non-matching Face را شبیهسازی کنید. این کار بدون اتصال دیوایس فیزیکی، تست تمام مسیرهای موفق و ناموفق را ممکن میسازد.
Android Emulator
در Android Studio Emulator، در پنجره Extended Controls تب Fingerprint یا با دستور adb این فرمان را اجرا کنید:
adb -e emu finger touch 1
عدد 1 شناسه اثر انگشت ثبتشده در شبیهساز است (در Settings → Security → Fingerprint اضافه کنید). برای تست خطای NotEnrolled، شبیهساز را ریست کنید و قبل از ثبت اثر انگشت اپ را اجرا کنید.
برای تستهای یکپارچه با Appium، باید بیومتریک را Mock کنید. هرگز در محیط CI/CD به API واقعی متصل نشوید. الگوی Mock کردن سرویسها در راهنمای تستنویسی .NET MAUI پوشش داده شده است.
خیر، .NET MAUI 10 هنوز API داخلی برای بیومتریک ندارد. باید از پلاگینهای جامعه مثل Plugin.Maui.Biometric یا Plugin.Fingerprint استفاده کنید که در پشتصحنه به LocalAuthentication در iOS و BiometricPrompt در اندروید متصل میشوند.
بهترین کتابخانه بیومتریک برای .NET MAUI در ۲۰۲۶ چیست؟
برای پروژههای جدید Plugin.Maui.Biometric پیشنهاد میشود چون نگهداری فعال دارد و از تمام APIهای جدید iOS 17 و Android 14 پشتیبانی میکند. Plugin.Fingerprint همچنان کار میکند ولی نام آن گمراهکننده است (Face ID هم پشتیبانی میکند).
چرا اپلیکیشن iOS من هنگام فراخوانی Face ID کرش میکند؟
چون کلید NSFaceIDUsageDescription در Info.plist وجود ندارد. iOS از سال ۲۰۱۷ بدون این کلید اپ را با Privacy Violation متوقف میکند. یک متن فارسی روشن (مثلاً «برای ورود سریع از Face ID استفاده میکنیم») به Info.plist اضافه کنید.
آیا میتوان بدون رمز عبور فقط با بیومتریک لاگین داشت؟
از نظر فنی بله، ولی توصیه نمیشود. بیومتریک «صاحب دستگاه» را تأیید میکند، نه هویت کاربر سرور را. الگوی صحیح این است که اولین لاگین با رمز انجام شود، توکن JWT در SecureStorage ذخیره شود و دفعات بعد بیومتریک فقط قفل این توکن را باز کند.
تفاوت BiometricPrompt و FingerprintManager در اندروید چیست؟
FingerprintManager از API 28 منسوخ شده و فقط با اثر انگشت کار میکند. BiometricPrompt از androidx.biometric جایگزین آن است و از تمام انواع بیومتریک (اثر انگشت، چهره، عنبیه) با یک API یکپارچه پشتیبانی میکند. در .NET MAUI 10 همیشه از BiometricPrompt استفاده کنید.
چگونه تشخیص دهیم کاربر اثر انگشت یا چهره جدیدی اضافه کرده است؟
پلاگین متدی به نام GetBiometryStateHashAsync() دارد که یک هش از وضعیت ثبتنام فعلی برمیگرداند. این هش را در زمان لاگین اول ذخیره کنید و هر بار قبل از باز کردن توکن مقایسه کنید؛ اگر تغییر کرد، توکن را پاک کنید و کاربر را به لاگین کامل هدایت کنید.
راهنمای عملی پیادهسازی احراز هویت JWT در .NET MAUI — ذخیره امن توکن با SecureStorage، تزریق خودکار با DelegatingHandler، محافظت مسیرها با Shell Navigation Guard و رفرش خودکار با Polly. همراه با کد کامل و تست.