المصادقة والأمان

التخزين الآمن باستخدام flutter_secure_storage

15 دقيقة الدرس 6 من 12

التخزين الآمن باستخدام flutter_secure_storage

تحتاج معظم تطبيقات Flutter إلى تخزين بيانات حساسة بصورة دائمة بين الجلسات — رموز المصادقة، ومفاتيح API، ورموز التحديث، وبيانات اعتماد المستخدم. تخزين هذه البيانات كنص عادي (على سبيل المثال عبر SharedPreferences) يُشكّل خطرًا أمنيًا جسيمًا. تحلّ هذه المشكلة حزمة flutter_secure_storage بكتابة البيانات إلى المخزن الآمن على مستوى نظام التشغيل: iOS Keychain على أجهزة Apple وAndroid Keystore المدعوم بتشفير على مستوى الأجهزة في Android.

ملاحظة: يخزّن SharedPreferences أزواج المفاتيح والقيم في ملف XML على Android (مجلد shared_prefs/) وفي NSUserDefaults على iOS. كلاهما نص عادي ويمكن قراءته من قِبل أي شخص لديه وصول إلى نظام ملفات الجهاز أو نسخة احتياطية. لا تخزّن أبدًا الرموز أو كلمات المرور فيهما.

المقارنة بين SharedPreferences و flutter_secure_storage

فهم الفرق بينهما أمر بالغ الأهمية قبل اختيار آلية التخزين:

  • SharedPreferences — غير مشفّر، مناسب للتفضيلات غير الحساسة (المظهر، اللغة، علامات الإعداد الأولي).
  • flutter_secure_storage — مشفّر بـ AES-256 على Android (عبر Keystore)، وKeychain على iOS. مناسب للرموز وكلمات المرور وأسرار API.
  • على Android، يُشفَّر المحتوى بمفتاح مخزَّن في Android Keystore ولا يغادر الأجهزة الآمنة على الأجهزة المدعومة.
  • على iOS، تُخزَّن العناصر في Keychain مع خيار إمكانية الوصول الذي تُهيّئه (مثل الوصول فقط عند إلغاء قفل الجهاز).

إضافة التبعية

أضف الحزمة إلى pubspec.yaml:

pubspec.yaml

dependencies:
  flutter_secure_storage: ^9.2.2

على Android يجب ضبط الحد الأدنى لإصدار SDK إلى 18 في android/app/build.gradle:

android/app/build.gradle

android {
    defaultConfig {
        minSdkVersion 18
        // ...
    }
}
نصيحة: على iOS، تستخدم flutter_secure_storage خدمات Keychain ولا تحتاج إلى أي إعداد إضافي. على Android 6+ (API 23+)، يكون مفتاح التشفير مدعومًا بـ Keystore محمي بالأجهزة. على الأجهزة الأقدم، ينتقل إلى RSA + AES برمجيًا.

واجهة برمجة التطبيقات الأساسية: القراءة والكتابة والحذف

الواجهة البرمجية بسيطة عن قصد. جميع الطرق async وتُعيد Future. أنشئ مثيلًا واحدًا من FlutterSecureStorage (أو استخدم نمط Singleton أو الحقن):

عمليات CRUD الأساسية

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenRepository {
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  static const _accessTokenKey  = 'access_token';
  static const _refreshTokenKey = 'refresh_token';

  // الكتابة / الكتابة فوق قيمة موجودة
  Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
  }) async {
    await _storage.write(key: _accessTokenKey,  value: accessToken);
    await _storage.write(key: _refreshTokenKey, value: refreshToken);
  }

  // القراءة (تُعيد null إذا لم يكن المفتاح موجودًا)
  Future<String?> getAccessToken() async {
    return _storage.read(key: _accessTokenKey);
  }

  // التحقق من الوجود دون قراءة القيمة
  Future<bool> hasSession() async {
    final token = await _storage.read(key: _accessTokenKey);
    return token != null && token.isNotEmpty;
  }

  // حذف مفتاح واحد
  Future<void> deleteAccessToken() async {
    await _storage.delete(key: _accessTokenKey);
  }

  // مسح جميع الإدخالات التي كتبها هذا التطبيق (استخدمها عند تسجيل الخروج)
  Future<void> clearAll() async {
    await _storage.deleteAll();
  }
}

خيارات خاصة بكل منصة

يمكنك تخصيص السلوك لكل منصة بتمرير كائنات الخيارات إلى المُنشئ. هذا مهم للتطبيقات الإنتاجية التي تحتاج سياسات وصول أو مصادقة محددة:

خيارات Android و iOS

final storage = FlutterSecureStorage(
  // Android: طلب مصادقة المستخدم قبل قراءة المفتاح
  aOptions: const AndroidOptions(
    encryptedSharedPreferences: true, // يستخدم واجهة برمجة EncryptedSharedPreferences
  ),
  // iOS: العنصر متاح فقط عند إلغاء قفل الجهاز
  iOptions: const IOSOptions(
    accessibility: KeychainAccessibility.unlocked,
  ),
);

// القراءة بنفس كائن الخيارات
Future<String?> readSensitiveKey(String key) async {
  return storage.read(key: key);
}
تحذير: على Android، إذا غيّرت AndroidOptions (مثل تبديل encryptedSharedPreferences) بعد كتابة بيانات بالفعل، تصبح الإدخالات القديمة غير قابلة للقراءة لأن مخطط التشفير مختلف. قم دائمًا بترحيل التخزين الحالي أو مسحه عند تغيير الخيارات في تحديث إنتاجي.

تخزين حالة المصادقة واستعادتها عند بدء التطبيق

نمط شائع هو التحقق من وجود رمز مخزَّن عند إطلاق التطبيق وإعادة التوجيه إلى الشاشة الرئيسية إذا كانت الجلسة لا تزال صالحة:

نمط Splash / Auth Gate

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  Future<bool> _checkSession() async {
    const storage = FlutterSecureStorage();
    final token = await storage.read(key: 'access_token');
    return token != null && token.isNotEmpty;
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: _checkSession(),
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }
        final isLoggedIn = snapshot.data ?? false;
        return isLoggedIn
            ? const HomeScreen()
            : const LoginScreen();
      },
    );
  }
}

أفضل ممارسات الأمان

  • استدعِ دائمًا deleteAll() عند تسجيل الخروج — لا تترك أبدًا رموزًا في التخزين بعد انتهاء الجلسة.
  • فضّل رموز الوصول قصيرة الصلاحية مع رموز التحديث، مع التدوير عند كل استخدام.
  • لا تخزّن كلمات مرور بنص عادي — استخدم الرموز أو بيانات الاعتماد المشفّرة فقط.
  • اجمع مع local_auth (القياسات الحيوية) لإضافة طبقة أمان إضافية قبل القراءة من Keystore/Keychain.
  • على Android، اضبط encryptedSharedPreferences: true لاستخدام واجهة برمجة EncryptedSharedPreferences من Jetpack Security بدلًا من مخطط RSA + AES القديم.
نصيحة: في اختبارات الوحدات، احقن تخزينًا وهميًا بدلًا من FlutterSecureStorage الحقيقي. تغليف الحزمة خلف واجهة مجردة (مثل abstract class SecureStorage) يجعل المستودع قابلًا للاختبار 100% دون لمس Keychain/Keystore الفعلي على الجهاز.

الخلاصة

flutter_secure_storage هي الحل القياسي لتخزين البيانات الحساسة في Flutter. تُغلّف iOS Keychain وAndroid Keystore خلف واجهة برمجية async نظيفة. استخدم write وread وdelete وdeleteAll لإدارة دورة حياة الأسرار كاملةً. فضّلها دائمًا على SharedPreferences لأي بيانات لا تريد لمهاجم محتمل قراءتها، واستدعِ deleteAll() عند تسجيل الخروج لعدم ترك أي بيانات اعتماد على الجهاز.