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

تقوية الأمان: أفضل الممارسات للشبكة والبيانات والمصادقة

16 دقيقة الدرس 12 من 12

تقوية الأمان: أفضل الممارسات للشبكة والبيانات والمصادقة

طوال هذا البرنامج التعليمي بنيت نظام مصادقة كاملاً. هذا الدرس الأخير يجمع كل شيء في قائمة تدقيق للدفاع المتعمق — تطبيق طبقات متعددة من الضوابط المستقلة بحيث إذا فشل أحد الإجراءات الوقائية، يبقى الآخرون لحماية المستخدمين. سنتناول تطبيق HTTPS الإلزامي، ونظافة الحقول الآمنة، وتسجيل الخروج الصحيح مع إلغاء الرموز، ومراجعة قواعد أمان Firebase.

1. تطبيق اتصالات HTTPS فقط

إرسال بيانات الاعتماد أو الرموز عبر HTTP العادي يعرّضها لهجمات الرجل في المنتصف (man-in-the-middle). عملاء HTTP في Flutter لا يطبّقون HTTPS افتراضياً، لذا يجب إضافة فحوصات صريحة.

عميل HTTP مخصص يرفض HTTP العادي

import 'package:http/http.dart' as http;

class SecureHttpClient extends http.BaseClient {
  final http.Client _inner = http.Client();

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    if (request.url.scheme != 'https') {
      throw StateError(
        'تم حظر الطلب غير الآمن: ${request.url}. '
        'يجب أن تستخدم جميع الطلبات HTTPS.',
      );
    }
    return _inner.send(request);
  }

  @override
  void close() => _inner.close();
}

// الاستخدام — تسجيل مرة واحدة عند بدء التطبيق
final secureClient = SecureHttpClient();

Future<Map<String, dynamic>> fetchUserProfile(String token) async {
  final response = await secureClient.get(
    Uri.parse('https://api.example.com/profile'),
    headers: {'Authorization': 'Bearer $token'},
  );
  if (response.statusCode == 200) {
    return jsonDecode(response.body) as Map<String, dynamic>;
  }
  throw Exception('فشل تحميل الملف الشخصي: ${response.statusCode}');
}
نصيحة: عند استخدام حزمة dio، أضف Interceptor مخصصاً يتحقق من أن options.uri.scheme == 'https' قبل كل طلب ويرمي DioException بخلاف ذلك. مركزة هذا في معترض يعني أن كل نقطة استدعاء محمية تلقائياً.

2. مسح الحافظة ونظافة الحقول الآمنة

حقول كلمات المرور وأكواد OTP والمفاتيح السرية غالباً ما ينسخها المستخدمون إلى الحافظة أو تملؤها خدمات إمكانية الوصول تلقائياً. ترك البيانات الحساسة في الحافظة بعد انتهاء الجلسة يُعدّ ناقل هجوم سهل التنفيذ.

مسح الحافظة عند تسجيل الخروج والتنقل

import 'package:flutter/services.dart';

/// استدعِ هذا عند تسجيل خروج المستخدم أو التخلص من شاشة المصادقة.
Future<void> clearSensitiveClipboard() async {
  // استبدل محتويات الحافظة بسلسلة فارغة.
  await Clipboard.setData(const ClipboardData(text: ''));
}

// في ودجت حقل كلمة المرور — امسح عند الإتلاف
class _SecurePasswordFieldState extends State<SecurePasswordField> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.clear();   // امسح مخزن التحكم
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      obscureText: true,
      // يجب أن يكون enableSuggestions و autocorrect كلاهما false
      // لمنع لوحة المفاتيح من تخزين النص المكتوب.
      enableSuggestions: false,
      autocorrect: false,
      keyboardType: TextInputType.visiblePassword,
      decoration: const InputDecoration(labelText: 'كلمة المرور'),
    );
  }
}
تحذير: ضبط obscureText: true وحده غير كافٍ. على Android، بعض لوحات المفاتيح لا تزال تسجّل ضغطات المفاتيح المخفية. اقرنه دائماً بـ enableSuggestions: false وautocorrect: false. على iOS، يُكبَح ذاكرة التخزين المؤقت للوحة المفاتيح تلقائياً لـ UITextContentTypePassword، والتي يربطها Flutter بـ TextInputType.visiblePassword.

3. تسجيل الخروج الصحيح مع إلغاء الرموز

مجرد التنقل بعيداً عن الشاشة الرئيسية ليس تسجيل خروج. تسجيل الخروج الحقيقي يجب أن: يلغي الرموز من جانب الخادم، ويمسح التخزين المحلي، ويمسح الحالة في الذاكرة، ويمسح مكدس التنقل بحيث لا يستطيع زر الرجوع العودة إلى الشاشات المصادق عليها.

تدفق تسجيل الخروج الكامل

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  /// تسجيل خروج كامل: إلغاء الرموز، مسح التخزين، مسح الذاكرة.
  Future<void> logout(BuildContext context) async {
    try {
      // 1. إلغاء رمز تحديث ID الخاص بـ Firebase من جانب الخادم.
      //    signOut() تجعل بيانات الاعتماد الحالية غير صالحة؛ أي
      //    رمز ID مخزن مؤقتاً لا يمكن تجديده بعد هذا الاستدعاء.
      await _auth.signOut();

      // 2. إذا أصدرت JWT خلفياً مخصصاً، ألغِه صراحةً.
      await _revokeBackendToken();

      // 3. امسح جميع البيانات الحساسة المستمرة محلياً.
      await _storage.deleteAll();

      // 4. امسح الحافظة في حالة نسخ بيانات الاعتماد.
      await Clipboard.setData(const ClipboardData(text: ''));

    } catch (e) {
      // سجّل لكن لا تعيد الرمي — أكمل التنظيف المحلي دائماً.
      debugPrint('خطأ تسجيل الخروج: $e');
    } finally {
      // 5. تنقل إلى تسجيل الدخول وأزل جميع المسارات من المكدس.
      if (context.mounted) {
        Navigator.of(context).pushNamedAndRemoveUntil(
          '/login',
          (route) => false,   // يزيل كل مسار سابق
        );
      }
    }
  }

  Future<void> _revokeBackendToken() async {
    final token = await _storage.read(key: 'refresh_token');
    if (token == null) return;
    await secureClient.post(
      Uri.parse('https://api.example.com/auth/revoke'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'refresh_token': token}),
    );
  }
}
ملاحظة: pushNamedAndRemoveUntil مع الشرط (route) => false يمسح مكدس التنقل بالكامل. بدون هذا، سيعيد زر الرجوع في Android المستخدم إلى المنطقة المصادق عليها من شاشة تسجيل الدخول — ثغرة أمنية شائعة في تطبيقات الجوال.

4. مراجعة قواعد أمان Firebase

قواعد أمان Firebase هي آخر خط دفاع من جانب الخادم. القواعد غير المُهيأة بشكل صحيح — مثل allow read, write: if true; — تكشف كل وثيقة للإنترنت بأسره. يجب أن تغطي مراجعة الأمان أربعة مجالات: فحوصات المصادقة، وتطبيق الملكية، والتحقق من البيانات، ونطاق أقل امتياز.

قالب قواعد أمان Firestore المُقوّى

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // --- دوال مساعدة ---
    function isAuthenticated() {
      return request.auth != null;
    }
    function isOwner(userId) {
      return isAuthenticated() && request.auth.uid == userId;
    }
    function isEmailVerified() {
      return isAuthenticated() && request.auth.token.email_verified == true;
    }
    function validUserWrite() {
      // السماح بكتابة الحقول المعروفة فقط؛ رفض الحقول الإضافية.
      return request.resource.data.keys().hasOnly([
        'displayName', 'avatarUrl', 'updatedAt'
      ]);
    }

    // --- ملفات المستخدمين الشخصية ---
    match /users/{userId} {
      // أي مستخدم مصادق يمكنه قراءة ملف شخصي.
      allow read: if isAuthenticated();
      // فقط المالك يمكنه الكتابة؛ يجب التحقق من البريد؛ التحقق من الحقول.
      allow write: if isOwner(userId)
                   && isEmailVerified()
                   && validUserWrite();
    }

    // --- بيانات المستخدم الخاصة ---
    match /users/{userId}/private/{doc} {
      // فقط المالك يمكنه قراءة أو كتابة مجموعته الفرعية الخاصة.
      allow read, write: if isOwner(userId);
    }

    // --- رفض كل شيء آخر افتراضياً ---
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
نصيحة: استخدم Firebase Emulator Suite لتشغيل اختبارات القواعد الآلية مع حزمة @firebase/rules-unit-testing قبل النشر. اكتب اختبارات تؤكد الحالتين المسموح بها والمرفوضة — قاعدة تجتاز اختبارات "المسموح" فقط قد تكون خطرة بشكل مفرط.

5. قائمة تدقيق تقوية إضافية

إلى جانب المجالات الأربعة الرئيسية أعلاه، طبّق هذه الضوابط التكميلية قبل الإصدار للإنتاج:

  • تثبيت الشهادات (Certificate pinning) — ضمّن هاش المفتاح العام لخادمك في التطبيق وارفض الشهادات التي لا تتطابق، مما يمنع إساءة استخدام هيئات الشهادات المارقة.
  • اكتشاف Root/Jailbreak — استخدم flutter_jailbreak_detection لتحذير المستخدمين أو تقييد الوظائف على الأجهزة المخترقة.
  • تعتيم إصدارات الإنتاج — شغّل flutter build apk --obfuscate --split-debug-info=build/debug-info لجعل الهندسة العكسية أصعب بكثير.
  • تجنب تخزين الأسرار في الكود — مفاتيح API في كود Dart يمكن استخراجها بسهولة من الثنائي المُجمَّع؛ استخدم نقاط نهاية الوكيل الخلفي أو Firebase Remote Config مع قواعد وصول من جانب الخادم.
  • أعمار قصيرة للرموز — احتفظ برموز ID قصيرة العمر (افتراضي Firebase: ساعة واحدة) واستخدم رموز التحديث فقط عبر HTTPS مع PKCE عند الانطباق.
  • مراجعة حزم الطرف الثالث — شغّل flutter pub audit بانتظام وثبّت إصدارات الحزم الحرجة في pubspec.lock.

ملخص

الدفاع المتعمق يعني عدم اعتبار أي ضابط واحد كافياً بمفرده. من خلال الجمع بين تطبيق HTTPS على طبقة الشبكة، ونظافة الحقول الآمنة على طبقة الإدخال، وإلغاء الرموز ومسح مكدس التنقل على طبقة الجلسة، وقواعد أمان Firebase الصارمة على طبقة البيانات، يكتسب نظام المصادقة الخاص بك مرونة حقيقية ضد أكثر ناقلات هجمات الجوال شيوعاً. طبّق قائمة التدقيق أعلاه قبل كل إصدار للإنتاج وراجعها كلما أضفت ميزات جديدة تمس بيانات اعتماد المستخدم.