Firebase 教學 - RealTime Database Rules 安全規則

透過 Rules ( 安全規則 ),我們可以讓 firebase 即時資料庫具有多一層的安全保護,規則除了可以指定哪些資料可以讀取或寫入,也可以搭配 auth 根據使用者進行規範,這篇文章將會介紹 RealTime Database Rules 的用法。

基本寫法

RealTime Database Rules 使用類似 JSON 格式的寫法,根據定義 .read.write.validate( .indexOn 也是屬性之一,但主要用途作為排序,在資料量不大時發揮不了作用,就不在此處介紹 )

類型 說明
.read 是否允許讀取資料,布林值判斷為 ture 可以讀取,false 不可讀取。
.write 是否允許寫入資料,布林值判斷為 ture 可以寫入,false 不可寫入。
.validate 判斷值、屬性、子屬性...等的正確格式。

舉例來說,在「Firebase 教學 - RealTime Database 安裝與使用 」一文裡建立的專案,使用的是最基本「沒有保護」的寫法,最外層是 rules 屬性,表示整份資料庫的規則,內容就會包含讀寫或資料夾相關的規則,因為.read.write都是 true,所以不論任何人都可以寫入或讀取資料。

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

同理,下面的寫法,只會讓「有註冊」的用戶 ( 具備 uid ) 才能夠讀取、寫入或刪除資料。( 後續會介紹如何依據使用者作保護 )

{
  "rules": {
    ".read": "auth.uid != null",
    ".write": "auth.uid != null"
  }
}

定義規則順序

Firebase 的規則採用「淺層判斷至深層」的做法,只要是存取資料,規則的 true 與 false 一律由淺層 ( root 根節點 ) 開始判斷起,舉例來說,下列規則的寫法,你以為在 a 節點的層級寫了 false 擋住了寫入,但實際上在 root 節點卻允許寫入,這也會造成 a 節點仍然可以寫入資料的情形。

{
  "rules": {
    ".read": true,
    ".write":true,
    "a": {
      ".read": false,
      ".write": false
    }
  }
}

如果使用 firebase 內建的模擬工具來看,可以看到當 root 節點已經允許,就算後方有設定規則 ( 不論規則為何 ),仍然可以寫入。

如果不希望淺層的規則影響到淺層,可以將相關規則設定留空,不要指定 true 或 false,如此一來就會自動採用深層的設定,舉例來說,下方的規則指定了 a 節點不可讀取和寫入,b 節點則可以讀取和寫入,如果不寫讀寫的規則,會變成預設值 false,false 不會影響深層,下方的例子除了 b 節點之外,其他節點一率無法讀去和寫入。

{
  "rules": {
    "a": {
      ".read": false,
      ".write": false
    },
    "b":{
      ".read": true,
      ".write": true
    }
  }
}

延續上面的例子,若將 root 節點與 a 節點的判斷對調,就會發現一開始先判斷 false,但遇到 a 層 true 又可以寫入的狀況。

初次接觸會不容易理解,其實可以換個角度想,遇到 false 得到 0,遇到 true 就加 1,如果最後結果大於 0 就表示可以寫入或讀取,例如上面的例子,一開始如果是 true,最後不論哪層寫了 false,得到的值都一定大於 0,也就表示一定可以寫入或讀取,反之如果是下方的例子,一開始是 0,緊接著遇到 true 加 1,在 a 節點就可以寫入或讀取,而其他節點因為沒有加 1 所以仍保持 0,也就不能讀取或寫入了。

{
  "rules": {
    ".read":false,
    ".write":false,
    "a": {
      ".read": true,
      ".write": true
    }
  }
}

上述淺層判斷至深層的做法只適用於.read.write,如果是.validate,則不受這個規則的限制,只會影響自己的層級,例如下方的規則,在 root 節點會「驗證新資料一定要包含 a 節點」,所以如果儲存的資料包含 a 節點,就可以通過驗證,但在 a 的層級卻會被「驗證新資料一定要是數字格式」所阻擋,導致資料一定得是 a 節點下的數字資料才能儲存的狀況。

{
  "rules": {
    ".read":true,
    ".write":true,
    ".validate": "newData.hasChildren(['a'])",
    "a": {
      ".read": false,
      ".write": false,
      ".validate": "newData.isNumber()"
    }
  }
}

預設變數 predefined variables

在安全規則裡頭,除了單純的撰寫節點名稱外,還有以下幾種預設的變數,每個變數都有其特定用法,因此在「節點」的命名上,都應該要避開這些名稱。

變數 說明 .read .write .validate
now 建立節點的時間 - - O
root 讀取或寫入資料之前,存在資料庫的根節點 O O -
newData 寫入資料之後會存在的資料,包含新的資料和原本的資料 - O O
data 讀取或寫入資料之前,存在資料庫的資料 O O O
$變數 通用變數符號,表示在某個節點內的所有節點 O O O
auth 身份驗證使用的變數 O O -

保護已存在的資料

透過使用者 auth 的規則,可以定義較為妥善的資料保護方法,但針對「匿名」的使用者,是否能讓「彈性」和「保護」都能兼具呢?針對匿名的使用者,大致上可以定義以下的保護條件:

  • 不能讀取整個資料庫,只能讀取某個節點 ( 表示知道名稱才能讀取 )
  • 不能刪除資料,只能不斷添加資料

根據這兩個條件,定義以下的規則,在 root 層禁止讀取和寫入,在之後的每個節點都可以讀取,不過如果該節點有資料,則無法寫入。

{
  "rules": {
    ".read": false,
    ".write": false,
    "$data": {
      ".read": true,
      ".write": "!data.exists()",
    }
  }
}

假設資料庫中已經有了下方的這些資料:

test: {
  a: 123
  b: 456
  c: 789
}

透過模擬可以發現,若要讀取整份 data 會被拒絕。

單獨讀取 a 節點的資料是被允許的。

若要寫入資料到 a 節點,因為 a 節點已經有資料,所以無法寫入。

不過如果是全新的節點,就可以寫入資料 ( 等同於使用 push 的功能 )

小結

Firebase RealTime Database 的規則其實相當簡單,只是要熟悉其邏輯和運作方式,透過規則就能保護資料庫,也就能做出更彈性的應用了。

更多參考:https://firebase.google.com/docs/reference/security/database/

有興趣瞧瞧其他新文章嗎?