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

本指南將延續「瞭解 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 AuthenticationFirebase Realtime Database 整合,讓你可以控管資料 使用條件,對個別使用者授予存取權。使用者完成驗證後,即時資料庫安全性規則中的 auth 變數就會填入使用者資訊。這項資訊包含專屬 ID (uid) 以及已連結的帳戶資料 (例如 Facebook ID 或電子郵件地址) 其他資訊。如果您導入了自訂驗證供應商,可以新增自己的欄位 使用者的驗證酬載

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

auth 變數

規則中的預先定義 auth 變數為空值, 驗證程序。

透過 Firebase 驗證驗證使用者之後 其中包含下列屬性:

供應商 使用的驗證方法 (「密碼」、「匿名」、「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"
      }
    }
  }
}

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

以撰寫資料庫的方式建構資料庫通常很有幫助 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()"

參照其他路徑中的資料

任何資料都可以用於規則的條件。使用預先定義的變數 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);
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. 運算式可用於即時資料庫安全性規則。

查詢規則運算式
運算式 類型 說明
query.orderByKey
query.orderByPriority
query.orderByValue
布林值 如果是依照鍵、優先順序或值排序的查詢,則為 True。否則為 False。
query.orderByChild 字串
null
使用字串來表示子節點的相對路徑。例如 query.orderByChild === "address/zip"。如果查詢未由子節點排序,這個值為空值。
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
擷取執行中查詢的邊界,或者傳回空值 (如果有的話) 未繫結。
query.limitToFirst
query.limitToLast
數字
null
擷取執行中查詢的限制,如果有,則傳回空值。 未設定限制

後續步驟

討論條件後,您對 Rules 的瞭解將更為深入,並可進行以下操作:

瞭解如何處理核心用途,並學習開發、 測試及部署 Rules

瞭解 Rules 專屬於 Realtime Database 的功能: