מדריך זה מתבסס על לימוד מדריך הליבה של כללי האבטחה של 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 משתלב עם Firebase Realtime Database כדי לאפשר לך לשלוט בגישה לנתונים על בסיס של משתמש תוך שימוש בתנאים. ברגע שמשתמש מאמת, משתנה auth
בכללי האבטחה של מסד הנתונים בזמן אמת יאוכלס במידע של המשתמש. מידע זה כולל את המזהה הייחודי שלהם ( uid
) וכן נתוני חשבון מקושרים, כגון מזהה פייסבוק או כתובת אימייל, ומידע אחר. אם אתה מיישם ספק אימות מותאם אישית, אתה יכול להוסיף שדות משלך למטען ההסמכה של המשתמש שלך.
סעיף זה מסביר כיצד לשלב את שפת כללי האבטחה של מסד הנתונים של Firebase בזמן אמת עם מידע אימות על המשתמשים שלך. על ידי שילוב שני המושגים הללו, תוכל לשלוט בגישה לנתונים על סמך זהות המשתמש.
משתנה auth
משתנה auth
המוגדר מראש בכללים הוא ריק לפני שהאימות מתבצע.
לאחר אימות של משתמש באמצעות Firebase Authentication הוא יכיל את המאפיינים הבאים:
ספק | שיטת האימות שבה נעשה שימוש ("סיסמה", "אנונימי", "פייסבוק", "github", "גוגל" או "טוויטר"). |
uid | מזהה משתמש ייחודי, מובטח להיות ייחודי בכל הספקים. |
אֲסִימוֹן | התוכן של אסימון Firebase Auth ID. עיין בתיעוד ההפניה עבור 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" } } } }
מבנה מסד הנתונים שלך כדי לתמוך בתנאי אימות
בדרך כלל מועיל לבנות את מסד הנתונים שלך בצורה שתקל על כתיבת כללים. דפוס נפוץ אחד לאחסון נתוני משתמש במסד הנתונים בזמן אמת הוא לאחסן את כל המשתמשים שלך בצומת users
בודד שהילדים שלו הם ערכי uid
עבור כל משתמש. אם תרצה להגביל את הגישה לנתונים האלה כך שרק המשתמש המחובר יוכל לראות את הנתונים שלו, הכללים שלך ייראו בערך כך.
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
עבודה עם תביעות מותאמות אישית של אימות
עבור אפליקציות הדורשות בקרת גישה מותאמת אישית עבור משתמשים שונים, אימות Firebase מאפשר למפתחים להגדיר תביעות על משתמש 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 Realtime Database. שקול את הכללים הבאים:
{ "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
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];
מָהִיר
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);
מנוחה
# 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 *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];
מָהִיר
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);
מנוחה
# 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.
ביטויים זמינים בכללי האבטחה של מסד נתונים בזמן אמת.
ביטויי כלל מבוססי שאילתה | ||
---|---|---|
ביטוי | סוּג | תיאור |
query.orderByKey query.orderByPriority query.orderByValue | בוליאני | נכון עבור שאילתות מסודרות לפי מפתח, עדיפות או ערך. שקר אחרת. |
query.orderByChild | חוּט ריק | השתמש במחרוזת כדי לייצג את הנתיב היחסי לצומת צאצא. לדוגמה, query.orderByChild === "address/zip" . אם השאילתה לא מסודרת על ידי צומת צאצא, ערך זה הוא null. |
query.startAt query.endAt query.equalTo | חוּט מספר בוליאני ריק | מאחזר את הגבולות של השאילתה המבצעת, או מחזיר null אם אין קבוצה מוגבלת. |
query.limitToFirst query.limitToLast | מספר ריק | מאחזר את המגבלה על השאילתה המבצעת, או מחזיר null אם לא הוגדרה מגבלה. |
הצעדים הבאים
לאחר הדיון הזה בתנאים, יש לך הבנה מתוחכמת יותר של הכללים ואתה מוכן:
למד כיצד לטפל במקרים בסיסיים של שימוש, ולמד את זרימת העבודה לפיתוח, בדיקה ופריסה של כללים:
- למד על הסט המלא של משתני כללים מוגדרים מראש שבהם אתה יכול להשתמש כדי לבנות תנאים .
- כתוב כללים המתייחסים לתרחישים נפוצים .
- בנה על הידע שלך על ידי סקירת מצבים שבהם אתה חייב לזהות חוקים לא בטוחים ולהימנע מהם .
- למד על Firebase Local Emulator Suite וכיצד אתה יכול להשתמש בה כדי לבדוק כללים .
- סקור את השיטות הזמינות לפריסת כללים .
למד תכונות כללים הספציפיות למסד נתונים בזמן אמת:
- למד כיצד להוסיף לאינדקס את מסד הנתונים בזמן אמת שלך .
- עיין בממשק API של REST לפריסת כללים .