在即時資料庫安全性規則中使用條件

本指南以「瞭解 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 Authentication,request.auth 變數會包含要求資料的用戶端驗證資訊。如要進一步瞭解 request.auth,請參閱參考說明文件

Firebase AuthenticationFirebase Realtime Database 整合,可讓您使用條件,以個別使用者為單位控管資料存取權。使用者完成驗證後,即時資料庫安全規則中的 auth 變數就會填入使用者資訊。這項資訊包括使用者的專屬 ID (uid)、連結的帳戶資料 (例如 Facebook ID 或電子郵件地址) 和其他資訊。如果您實作自訂驗證供應商,可以在使用者的驗證酬載中新增自己的欄位。

本節說明如何將 Firebase 即時資料庫安全規則語言與使用者驗證資訊結合。結合這兩個概念,您就能根據使用者身分控管資料存取權。

auth 變數

規則中預先定義的 auth 變數在驗證前為空值。

使用者透過 Firebase 驗證完成驗證後,會包含下列屬性:

供應商 使用的驗證方法 (「password」、「anonymous」、「facebook」、「github」、「google」或「twitter」)。
uid 專屬使用者 ID,保證在所有供應商中都是不重複的。
token Firebase 驗證 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"
      }
    }
  }
}

建構資料庫以支援驗證條件

通常,以利於寫入的方式建構資料庫會很有幫助。RulesRealtime 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()"

參照其他路徑中的資料

任何資料都可以做為規則的條件。使用預先定義的變數 rootdatanewData,我們可以存取寫入事件前後的任何路徑。

請參考以下範例,只要 /allow_writes/ 節點的值為 true、父項節點未設定 readOnly 旗標,且新寫入的資料中有名為 foo 的子項,即可執行寫入作業:

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

驗證資料

強制執行資料結構,並驗證資料格式和內容時,應使用 .validate 規則,只有在 .write 規則成功授予存取權後,才會執行這些規則。以下是規則定義範例,只允許 1900 年至 2099 年間的 YYYY-MM-DD 格式日期,並使用規則運算式進行檢查。.validate

".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()"
      }
    }
  }
}

請參考下列寫入作業的結果,並留意這個變體:

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);
注意:這個 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()"
      }
    }
  }
}

在這個變體中,下列任一作業都會成功:

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);
注意:這個 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. 運算式。

以查詢為準的規則運算式
Expression 類型 說明
query.orderByKey
query.orderByPriority
query.orderByValue
布林值 如果查詢是依鍵、優先順序或值排序,則為 True。否則為 False。
query.orderByChild 字串
null
使用字串表示子項節點的相對路徑。例如:query.orderByChild === "address/zip"。如果查詢不是依子項節點排序,這個值會是空值。
query.startAt
query.endAt
query.equalTo
字串
數字
布林值
空值
擷取執行中查詢的界線,如果未設定界線,則傳回 null。
query.limitToFirst
query.limitToLast
數字
null
擷取執行查詢的限制,如果未設定限制,則傳回 null。

後續步驟

討論完條件後,您將對 Rules 有更深入的瞭解,並準備好:

瞭解如何處理核心用途,以及開發、測試及部署 Rules 的工作流程:

  • 瞭解可用於建構條件的完整預先定義Rules 變數集
  • 撰寫規則來因應常見情境
  • 請參閱這篇文章,瞭解在哪些情況下必須找出並避免使用不安全的規則,進一步擴展相關知識。
  • 瞭解 Firebase 本機模擬器套件,以及如何使用該套件測試Rules
  • 查看部署Rules的可用方法。

瞭解 Rules 專屬的 Realtime Database 功能: