استخدام الشروط في قواعد أمان قاعدة البيانات في الوقت الفعلي

يستند هذا الدليل إلى دليل تعلُّم لغة "قواعد أمان Firebase" الأساسية لتوضيح كيفية إضافة شروط إلى "قواعد أمان قاعدة بيانات Firebase في الوقت الفعلي".

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

  • التحقّق من مصادقة المستخدم
  • تقييم البيانات الحالية مقارنةً بالبيانات التي تم إرسالها حديثًا
  • الوصول إلى أجزاء مختلفة من قاعدة البيانات ومقارنتها
  • التحقّق من صحة البيانات الواردة
  • استخدام بنية طلبات البحث الواردة لمنطق الأمان

استخدام المتغيّرات التي تبدأ بـ $ لتسجيل أجزاء المسار

يمكنك التقاط أجزاء من المسار للقراءة أو الكتابة من خلال تعريف متغيّرات الالتقاط باستخدام البادئة $. يُستخدَم هذا كحرف بدل، ويخزّن قيمة هذا المفتاح لاستخدامه داخل شروط القواعد:

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

يمكن أيضًا استخدام متغيّرات $ الديناميكية بالتوازي مع أسماء المسارات الثابتة. في هذا المثال، نستخدم المتغيّر $other للإعلان عن قاعدة .validate تضمن أنّ widget ليس لديه عناصر فرعية غير title وcolor. وستفشل أي عملية كتابة تؤدي إلى إنشاء عناصر فرعية إضافية.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

المصادقة

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

إذا كان تطبيقك يستخدم مصادقة Firebase، يحتوي المتغيّر request.auth على معلومات المصادقة للعميل الذي يطلب البيانات. لمزيد من المعلومات حول "request.auth"، يُرجى الاطّلاع على المستندات المرجعية.

يتكامل Firebase Authentication مع Firebase Realtime Database للسماح لك بالتحكّم في الوصول إلى البيانات لكل مستخدم على حدة باستخدام الشروط. بعد مصادقة المستخدم، سيتم ملء المتغيّر auth في قواعد أمان قاعدة بيانات الوقت الفعلي بمعلومات المستخدم. وتشمل هذه المعلومات المعرّف الفريد (uid) بالإضافة إلى بيانات الحساب المرتبط، مثل معرّف Facebook أو عنوان البريد الإلكتروني، وغيرها من المعلومات. إذا كنت تستخدم موفّر مصادقة مخصّصًا، يمكنك إضافة الحقول الخاصة بك إلى حمولة المصادقة الخاصة بالمستخدم.

يشرح هذا القسم كيفية دمج لغة قواعد أمان "قاعدة بيانات Firebase في الوقت الفعلي" مع معلومات المصادقة الخاصة بالمستخدمين. من خلال الجمع بين هذين المفهومين، يمكنك التحكّم في الوصول إلى البيانات استنادًا إلى هوية المستخدم.

متغيّر auth

يكون المتغيّر auth المحدّد مسبقًا في القواعد فارغًا قبل إجراء المصادقة.

بعد مصادقة المستخدم من خلال مصادقة Firebase، سيحتوي على السمات التالية:

مزوِّد الخدمة طريقة المصادقة المستخدَمة ("password" أو "anonymous" أو "facebook" أو "github" أو "google" أو "twitter")
uid رقم تعريف المستخدم الفريد، ويُضمن أن يكون فريدًا لدى جميع مقدّمي الخدمات.
رمز مميز محتوى الرمز المميّز لمعرّف Firebase Auth لمزيد من التفاصيل، يمكنك الاطّلاع على المستندات المرجعية حول auth.token.

في ما يلي مثال على قاعدة تستخدم المتغيّر auth لضمان أنّه يمكن لكل مستخدم الكتابة فقط في مسار خاص به:

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

تنظيم قاعدة البيانات لتتوافق مع شروط المصادقة

من المفيد عادةً تنظيم قاعدة البيانات بطريقة تسهّل كتابة Security Rules. من الأنماط الشائعة لتخزين بيانات المستخدمين في Realtime Database تخزين جميع المستخدمين في عقدة users واحدة تكون عناصرها الفرعية هي قيم uid لكل مستخدم. إذا أردت تقييد الوصول إلى هذه البيانات بحيث لا يتمكّن سوى المستخدم الذي سجّل الدخول من الاطّلاع على بياناته، ستكون قواعدك على النحو التالي:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

التعامل مع المطالبات المخصّصة للمصادقة

بالنسبة إلى التطبيقات التي تتطلّب تحكّمًا مخصّصًا في الوصول لمستخدمين مختلفين، تتيح Firebase Authentication للمطوّرين ضبط المطالبات لمستخدم Firebase. يمكن الوصول إلى هذه المطالبات في المتغيّر auth.token في قواعدك. في ما يلي مثال على القواعد التي تستخدم المطالبة المخصّصة hasEmergencyTowel:

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

يمكن للمطوّرين الذين ينشئون رموز المصادقة المخصّصة الخاصة بهم إضافة مطالبات إلى هذه الرموز اختياريًا. تتوفّر هذه المطالبات في المتغيّر auth.token في قواعدك.

البيانات الحالية مقابل البيانات الجديدة

يُستخدَم المتغيّر المُحدَّد مسبقًا data للإشارة إلى البيانات قبل إجراء عملية كتابة. في المقابل، يحتوي المتغيّر newData على البيانات الجديدة التي ستكون موجودة في حال نجاح عملية الكتابة. يمثّل newData النتيجة المدمجة للبيانات الجديدة التي تتم كتابتها والبيانات الحالية.

للتوضيح، تسمح لنا هذه القاعدة بإنشاء سجلّات جديدة أو حذف سجلّات حالية، ولكنّها لا تسمح لنا بإجراء تغييرات على البيانات الحالية غير الفارغة:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

الإشارة إلى البيانات في مسارات أخرى

يمكن استخدام أي بيانات كمعيار للقواعد. باستخدام المتغيرات المحددة مسبقًا root وdata وnewData، يمكننا الوصول إلى أي مسار كما هو موجود قبل أو بعد حدث الكتابة.

لنفترض أنّ لدينا المثال التالي الذي يسمح بعمليات الكتابة طالما أنّ قيمة العقدة /allow_writes/ هي true، والعقدة الرئيسية لا تحتوي على علامة readOnly، وهناك عقدة فرعية باسم foo في البيانات المكتوبة حديثًا:

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

التحقق من صحة البيانات

يجب فرض هياكل البيانات والتحقّق من صحة تنسيق البيانات ومحتواها باستخدام قواعد .validate التي يتم تشغيلها فقط بعد نجاح قاعدة .write في منح إذن الوصول. في ما يلي نموذج لتعريف قاعدة .validate يسمح فقط بالتواريخ بتنسيق YYYY-MM-DD بين العامَين 1900 و2099، ويتم التحقّق منه باستخدام تعبير عادي.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

قواعد .validate هي النوع الوحيد من قواعد الأمان التي لا يتم تطبيقها بشكل متسلسل. إذا تعذّر تطبيق أي قاعدة تحقّق على أي سجلّ فرعي، سيتم رفض عملية الكتابة بأكملها. بالإضافة إلى ذلك، يتم تجاهل تعريفات التحقّق من الصحة عند حذف البيانات (أي عندما تكون القيمة الجديدة التي تتم كتابتها هي null).

قد تبدو هذه النقاط تافهة، ولكنها في الواقع ميزات مهمة لكتابة قواعد أمان قوية لقاعدة بيانات Firebase في الوقت الفعلي. يُرجى مراعاة القواعد التالية:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

مع وضع هذا المتغير في الاعتبار، انظر إلى نتائج عمليات الكتابة التالية:

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

لنلقِ نظرة الآن على البنية نفسها، ولكن باستخدام قواعد .write بدلاً من .validate:

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

في هذا الخيار، ستنجح أي من العمليات التالية:

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

يوضّح هذا المثال الاختلافات بين قواعد .write و.validate. كما هو موضّح، يجب كتابة كل هذه القواعد باستخدام .validate، مع استثناء محتمل لقاعدة newData.hasChildren()، والتي تعتمد على ما إذا كان يجب السماح بعمليات الحذف.

القواعد المستندة إلى طلبات البحث

على الرغم من أنّه لا يمكنك استخدام القواعد كفلاتر، يمكنك حصر الوصول إلى مجموعات فرعية من البيانات باستخدام مَعلمات طلب البحث في قواعدك. استخدِم تعبيرات query. في قواعدك لمنح إذن بالقراءة أو الكتابة استنادًا إلى مَعلمات طلب البحث.

على سبيل المثال، تستخدم القاعدة التالية المستندة إلى طلب البحث قواعد الأمان المستندة إلى المستخدم والقواعد المستندة إلى طلب البحث لتقييد الوصول إلى البيانات في مجموعة baskets لتشمل فقط سلال التسوّق التي يملكها المستخدم النشط:

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

سينجح طلب البحث التالي الذي يتضمّن معلمات طلب البحث في القاعدة:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

ومع ذلك، فإنّ طلبات البحث التي لا تتضمّن المَعلمات في القاعدة ستتعذّر مع ظهور الخطأ PermissionDenied:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

يمكنك أيضًا استخدام القواعد المستندة إلى طلبات البحث للحدّ من كمية البيانات التي ينزّلها العميل من خلال عمليات القراءة.

على سبيل المثال، تحدّ القاعدة التالية من إذن الوصول للقراءة إلى أول 1000 نتيجة فقط من نتائج طلب البحث، حسب الأولوية:

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

تتوفّر التعبيرات query. التالية في "قواعد أمان قاعدة بيانات الوقت الفعلي".

تعبيرات القواعد المستندة إلى طلبات البحث
Expression النوع الوصف
query.orderByKey
query.orderByPriority
query.orderByValue
قيمة منطقية تكون القيمة "صحيح" لطلبات البحث التي يتم ترتيبها حسب المفتاح أو الأولوية أو القيمة. ويتم عرض القيمة "خطأ" إذا لم تكن نقطة الإدخال داخله.
query.orderByChild string
null
استخدِم سلسلة لتمثيل المسار النسبي لعقدة فرعية. على سبيل المثال، query.orderByChild === "address/zip". إذا لم يتم ترتيب طلب البحث حسب عقدة فرعية، تكون هذه القيمة فارغة.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Retrieves the bounds of the executing query, or returns null if there is no bound set.
query.limitToFirst
query.limitToLast
number
null
يستردّ الحدّ الأقصى لعدد النتائج في طلب البحث الذي يتم تنفيذه، أو يعرض قيمة فارغة في حال عدم ضبط حدّ أقصى.

الخطوات التالية

بعد هذه المناقشة حول الشروط، أصبح لديك فهم أكثر تعقيدًا لـ Security Rules وأصبحت جاهزًا لما يلي:

تعرَّف على كيفية التعامل مع حالات الاستخدام الأساسية، وتعرَّف على سير العمل لتطوير Security Rules واختباره ونشره:

تعرَّف على ميزات "Security Rules" الخاصة بـ "Realtime Database":