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 | - |
now
now
表示 firebase 資料庫寫入的時間,單位採用毫秒計算 ( 也就是從 1970 年 1 月 1 日到現在的毫秒數 ),從下列的規則,可以指定使用者寫入的資料,需要有一個 time 的節點放置當前的毫秒數,且這個毫秒數一定要在 now 之後。{ "rules": { ".read":false, ".write":true, ".validate":"newData.child('time').val()<now" } }
root
root
表示根節點,通常會搭配像是.child
的指令來輔助判斷,以下面的例子來說,如果資料庫內有 a 節點,且 a 節點內的 aa 節點的值為 ok,才能夠讀取資料,如此一來我們也可以透過外部寫入資料改變 aa 節點,來讓整份資料庫是可讀或不可讀。{ "rules": { ".read":"root.child('a').child('aa').val() == 'ok'", ".write":true } }
newData
newData
表示新資料,新資料的判斷只會用於.write
和.vaildate
,下方的規則,會限制新的資料一定得包含 a 和 b 節點才能寫入。{ "rules": { ".read":true, ".write":"newData.hasChildren(['a','b'])" } }
data
data
表示已經存在的資料,可以用在.read
、.write
和.vaildate
,下方的規則,會判斷目前資料庫中 a 節點是否存在 lock 為 true 的資料,如果有,則無法寫入,如果沒有就可以寫入。{ "rules": { ".read":true, "a":{ ".write":"data.child('lock').val() != true" } } }
$變數
$變數
的意思是通用變數符號,也就是不論該層節點如何命名,使用$變數
就代表該層所有的節點名稱,舉例來說,下方的規則雖然是$a
,但表示得並不是 a 節點,而是在 root 下一層的「所有節點」都能寫入或讀取資料。{ "rules": { ".read": false, ".write": false, "$a": { ".read": true, ".write": true } } }
auth
auth
用於搭配 firebase 專案的註冊帳號功能,透過判斷註冊的帳號、uid...等相關資訊,決定是否可以讀取或寫入資料,下方的規則,會判斷帳號的 uid 是否等於節點的名稱或者 uid 是否存在,如果不等於則不會寫入,如果 uid 相等則會寫入,如果 uid 不存在則會新建一個名稱為 uid 的節點。因為第一層是
.write
,若判斷為 true 則後方不論.write
規則為何都可以寫入,所以第二層使用.validate
就能避免「淺層判斷至深層」。( 請參考本篇文章上面的說明 ){ "rules": { ".write":"auth.uid != null", "$user": { ".read": true, ".validate":"$user === auth.uid" } } }
如果沒有 uid ,在第一層就會被排除。
如果某個 uid 的節點名稱不等於使用者的 uid 時,這個節點不能寫入資料。
如果資料庫中已經有 uid 的節點,當節點名稱等於使用者的 uid 時,這個節點就可以寫入資料。
保護已存在的資料
透過使用者 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/