實時數據庫安全規則中的使用條件

本指南以學習核心 Firebase 安全規則語言指南為基礎,展示如何向 Firebase 實時數據庫安全規則添加條件。

實時數據庫安全規則的主要構建塊是條件。條件是一個布爾表達式,用於確定是允許還是拒絕特定操作。對於基本規則,使用truefalse文字作為條件非常有效。但是實時數據庫安全規則語言為您提供了編寫更複雜條件的方法,這些條件可以:

  • 檢查用戶身份驗證
  • 根據新提交的數據評估現有數據
  • 訪問和比較數據庫的不同部分
  • 驗證傳入數據
  • 將傳入查詢的結構用於安全邏輯

使用 $ 變量捕獲路徑段

您可以通過使用$前綴聲明捕獲變量來捕獲用於讀取或寫入的部分路徑。這用作通配符,並存儲該鍵的值以供在規則條件內使用:

{
  "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除了titlecolor之外沒有其他孩子。任何會導致創建額外子項的寫入都將失敗。

{
  "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 Security Rules 規則中的auth變量將填充用戶信息。此信息包括他們的唯一標識符 ( uid ) 以及鏈接的帳戶數據,例如 Facebook id 或電子郵件地址,以及其他信息。如果您實施自定義身份驗證提供程序,則可以將您自己的字段添加到用戶的身份驗證有效負載中。

本節介紹如何將 Firebase 實時數據庫安全規則語言與有關用戶的身份驗證信息相結合。通過結合這兩個概念,您可以根據用戶身份控制對數據的訪問。

auth變量

在進行身份驗證之前,規則中預定義的auth變量為 null。

使用Firebase 身份驗證對用戶進行身份驗證後,它將包含以下屬性:

供應商使用的身份驗證方法(“密碼”、“匿名”、“facebook”、“github”、“google”或“twitter”)。
uid唯一的用戶 ID,保證在所有提供商中都是唯一的。
令牌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()"

引用其他路徑中的數據

任何數據都可以用作規則的標準。使用預定義變量rootdatanewData ,我們可以訪問寫入事件之前或之後存在的任何路徑。

考慮這個例子,它允許寫操作,只要/allow_writes/節點的值為true ,父節點沒有設置readOnly標誌,並且在新寫入的數據中有一個名為foo的子節點:

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

驗證數據

應使用.validate規則執行數據結構並驗證數據的格式和內容,這些規則僅在.write規則成功授予訪問權限後運行。下面是一個示例.validate規則定義,它只允許 1900-2099 年之間格式為 YYYY-MM-DD 的日期,並使用正則表達式進行檢查。

".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);
目標-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];
迅速
注意:此 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);
爪哇
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);
目標-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];
迅速
注意:此 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)
爪哇
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.表達式在實時數據庫安全規則中可用。

基於查詢的規則表達式
表達類型描述
查詢.orderByKey
查詢.orderByPriority
查詢.orderByValue
布爾值對於按鍵、優先級或值排序的查詢為真。否則為假。
query.orderByChild細繩
無效的
使用字符串表示子節點的相對路徑。例如, query.orderByChild === "address/zip" 。如果查詢未按子節點排序,則此值為空。
查詢.startAt
查詢.endAt
查詢.等於
細繩
數字
布爾值
無效的
檢索執行查詢的邊界,如果沒有邊界集則返回 null。
查詢.limitToFirst
查詢.limitToLast
數字
無效的
檢索執行查詢的限制,如果未設置限制,則返回 null。

下一步

在討論了條件之後,您對規則有了更深入的了解,並準備好:

了解如何處理核心用例,並了解開發、測試和部署規則的工作流程:

學習特定於實時數據庫的規則功能: