iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

Part 3|第 8/30 篇
今日要做的事:Firebase 安全性規則(Security Rules),用 Emulator 證明 uid-A 讀不到 uid-B。
今天要解決的目的: 隔離靠伺服器規則,不靠「前端沒顯示別人的按鈕」。

Day 6 讓資料有了身份,但還沒證明身份真的能隔離資料。
昨天 Firestore 還在測試模式——誰拿到路徑都能動。
你總不會希望隔壁桌的人偷看你冰箱庫存、順便改你過敏資料吧?XDD
所以今天我要看到紅燈:uid-A 讀 uid-B → 必須被拒絕

官方怎麼定義這件事:Security Rules 是伺服器端強制執行的規則,用來保護 Firestore/Storage 等資料——不是請前端「乖乖不要亂讀」。文件在這:
https://firebase.google.com/docs/rules?hl=zh-tw


今日任務卡

項目 內容
產出 firestore.rules、五個 allow/deny 測試、Emulator 留證
工具 Security Rules、Emulator Suite、@firebase/rules-unit-testing
不使用 真實過敏/照片、前端藏按鈕當安全、先 deploy 再測
完成條件 A→A 綠、A→B 紅、未登入紅

本機程式:material/code/day7


路線圖

問題:路徑有 uid ≠ 有權限
  → 設計:整棵 users/{userId} 只有本人
  → 寫 firestore.rules
  → CLI 丟進 Emulator 測
  → npm test 五案全過
  → Emulator UI 確認測的是本機
  → (之後)再考慮 deploy 正式專案

1. 問題:前端傳來的 uid 不能當身份

路徑寫成 users/{uid}/daily/... 只是資料夾整理,不會自動產生權限
client 寫錯或惡意改路徑,照樣可以指向別人的 uid——UI 藏按鈕擋不住會開 DevTools 的人。

https://ithelp.ithome.com.tw/upload/images/20260922/20121052OYaegI8hGI.jpg

真正能信的只有:

request.auth.uid == path 裡的 userId

文件裡的 owner_uid、網址參數、畫面上選到的帳號——都不能替代 Auth token。

為什麼今天一定要測「被拒絕」?

官方文件把 Security Rules 講成「細微的伺服器強制執行規則」。我用工程師白話翻譯成三條,對應今天的測試:

官方/規則觀念 人話 今天哪一案在證明
伺服器端執行 規則跑在 Firebase,不跑在你漂亮的前端 整份 npm test(Emulator 模擬同一套門衛)
沒允許=拒絕 沒寫到的路徑預設 deny,不是「沒寫就隨便進」 未登入讀 A → deny
身份來自 Auth token 跟路徑比的是 request.auth.uid,不是表單自填 A 讀/寫 B → deny;偽造 owner_uid 也救不了
跟 Auth 搭配 先登入才有 uid 可比;規則不管「推薦合不合理」 A 讀/寫 A → allow(本人該過)

再講白一點:DishFlow 以後會有 pantry、過敏、今天吃了什麼。
那些是個人資料。我不會希望陌生人(或另一個測試帳)把我的庫存當開放 API。
所以今天測的不是「規則檔長得漂不漂亮」,而是——門衛會不會真的把門關上。

今天只解資料隔離。欄位合不合 schema,還是 Part 2 validator 的事;推薦能不能出現某食材,是後面 Agent C 的 hard constraint。


2. 設計:整棵子樹只有本人

v1 私有資料都掛在:

users/{userId}/{document=**}

最低要測這四種情況:

https://ithelp.ithome.com.tw/upload/images/20260922/20121052f58lBQRIwz.jpg

情境 預期
uid-A 讀/寫自己 allow
uid-A 讀/寫 uid-B deny
未登入讀 users deny
寫 uid-B 路徑,body 卻寫 owner_uid: A 仍 deny

兩道門別搞混:

管什麼
Day 7 Rules 誰能碰哪份資料(uid-A 不能讀 uid-B)
Agent C hard constraint 推薦內容能不能出現某食材

Rules 全綠,不代表推薦已經安全。


3. 實作:規則很小,測試要兇

3.1 firestore.rules

對應官方說的 Firebase 安全性規則——我這份是套在 Cloud Firestore 上的門衛(同一套概念之後也會用在 Storage)。

這不是 App UI,也不是資料庫裡的某一筆。
路徑長得像屬於某人,不代表別人就不能偷讀。所以我規定:token 裡的 uid 必須等於路徑上的 {userId},才准動整棵子樹——冰箱、過敏、今日菜單,一律同款門禁。

這行 意思
rules_version = '2' 第 2 代規則語法
match /users/{userId}/{document=**} 管住此人底下所有子孫(profile、daily、以後的 pantry…)
request.auth != null 必須已登入
request.auth.uid == userId 登入者=路徑上的那個人

沒寫到的路徑預設 deny

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // v1:整棵 users/{userId} 子樹只有本人能讀寫
    match /users/{userId}/{document=**} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }
  }
}

3.2 規則檔 vs Firebase CLI

規則寫在檔案裡;CLI 負責拿去測或發布:

https://ithelp.ithome.com.tw/upload/images/20260922/20121052fWxmZV2zS2.jpg

指令 做什麼 我什麼時候用
npm testemulators:exec 本機套用規則+跑測試 今天主線
npm run emulators 開 Emulator+瀏覽器 UI 想肉眼看本機狀態
firebase deploy --only firestore:rules 發布到正式雲端 本地全綠之後再說

Console 網頁也能改 Rules,但難跟專案一起版控,也難自動化測跨帳號 deny。
我選:檔案為準 → CLI 測試 → 需要時再 deploy

3.3 本機資料夾

https://ithelp.ithome.com.tw/upload/images/20260922/20121052NFaaPmEy0d.jpg

material/code/day7/
  firestore.rules          ← 門衛規則
  firebase.json            ← 告訴 CLI 規則在哪、Emulator 埠號
  package.json
  tests/rules.test.js      ← 測試台(不是產品 UI)

測試資料只用 fixture:users/uid-a/...users/uid-b/...
seed 才暫時關 rules;正式斷言一律用「假裝已登入 A」/「未登入」的 context。

3.4 怎麼跑測試

cd material/code/day7
npm install
npm test

npm test=啟動 Firestore Emulator → 跑五案 → 關掉。

Emulator 需要 Java。若出現 Could not spawn java -version,裝好 JDK 後重開終端再跑。

3.5 測試台在測什麼——為什麼這五案有意義

rules.test.js 不是產品畫面,只是專門拿來撞規則的測試台。
官方文件也強調要用模擬器/測試把規則驗證掉;我今天做的就是這件事:假裝不同身份去撞門,看會不會真的被擋。

對照「別偷看我的庫存」心智模型:

案例 在演哪種人生 預期
A 讀/寫 A 我看自己的冰箱——應該可以 allow ✓
A 讀 B 我偷看別人庫存——應該被轟出去 deny ✓
A 寫 B(還偽造 owner_uid 我硬改別人資料,還在 body 假裝「這是我的」——門衛看的是路徑+token,不是你自介 deny ✓
未登入讀 A 路人連登入都沒登就伸手——直接拒絕 deny ✓
const dbA = env.authenticatedContext("uid-a").firestore();
const dbGuest = env.unauthenticatedContext().firestore();

await assertSucceeds(getDoc(doc(dbA, "users", "uid-a", "profile", "main")));
await assertSucceeds(setDoc(doc(dbA, "users", "uid-a", "daily", "fixture-day"), { ok: true }));
await assertFails(getDoc(doc(dbA, "users", "uid-b", "profile", "main")));
await assertFails(
  setDoc(doc(dbA, "users", "uid-b", "daily", "fixture-day"), { owner_uid: "uid-a" })
);
await assertFails(getDoc(doc(dbGuest, "users", "uid-a", "profile", "main")));

https://ithelp.ithome.com.tw/upload/images/20260922/20121052OutSuE38Pt.png

跑的時候若冒出 PERMISSION_DENIED,那是跨帳號被擋的 log——測試預期如此,不是失敗。


4. Emulator UI:本機假控制台

npm run emulators 之後,瀏覽器開 http://127.0.0.1:4000,就是 Firebase Emulator Suite UI
我這次畫面裡 Firestore 是 On / Port 8080,其餘服務 Off——今天本來就只開 Firestore。

https://ithelp.ithome.com.tw/upload/images/20260922/201210524L6TOEqfwC.jpg

https://ithelp.ithome.com.tw/upload/images/20260922/20121052pIsEoYzCtT.jpg

它是本機的「假 Firebase 控制台」:資料跑在 Emulator 裡,不是正式專案 dishflow 的雲端。頁面上的黃條也寫了 local environment。

功能 說明 今天有沒有用到
Overview 看誰 On 確認 Firestore Emulator 有起來
Firestore 分頁 瀏覽本機假資料、路徑 可看可不看
Authentication 分頁 本機假帳號 沒開 Auth emulator
Logs Emulator 請求/錯誤 可看可不看
自動跑五案斷言 做不到,那是 npm test 的事
自動發布正式環境 不會;關掉通常就清空
npm test Emulator UI
目的 自動證明 allow/deny 確認測的是本機假雲端
輸出 Terminal 五個 ✓ 瀏覽器 Overview
會不會改正式資料 不會 不會

Terminal 偶爾出現 demo-no-projectfirebase login 之類提示,本地測 Rules 可以略過,不影響今天的斷言。


5. 翻車提醒

  1. 整支測試包在 withSecurityRulesDisabled()——那只適合 seed。
  2. 只測讀、忘了測跨 uid 寫。
  3. Emulator 全綠就以為正式環境已安全——還要另外 deploy Rules;本地通過與已發布是兩件事。
  4. 為了省事寫 allow read: if true——等於回到測試模式。

6. 今日結論

  1. 今天玩的就是官方的 Firebase Security Rules——伺服器端門衛,不是前端禮貌。
  2. uid 隔離靠 token 比路徑;你總不會希望別人偷看你的庫存吧。
  3. npm test 證明紅燈會亮;Emulator UI 證明測的是本機。
  4. 本地通過 ≠ 正式專案已換掉測試模式。

下一篇: 把餐點照片放進相同 uid 邊界的私有 Storage,並保留無照片的手動後門。


附錄 A:素材對照

檔/畫面 用途
day7-0104day7-06 概念說明圖
編輯器裡的 firestore.rules 露出 request.auth.uid == userId
day7-05-npm-test.png Terminal 五案全過
day7-07-emulator-ui-shot.png Emulator UI · Firestore On
(可選)Console Playground uid-A 讀 uid-B → denied
(可選)正式發布後的 Rules 頁 與本地測試分開標示

附錄 B:firebase.json(節錄)

{
  "firestore": { "rules": "firestore.rules" },
  "emulators": {
    "firestore": { "host": "127.0.0.1", "port": 8080 },
    "ui": { "enabled": true, "port": 4000 }
  }
}

附錄 C:今天狀態

  1. [x] npm install
  2. [x] Java 可用
  3. [x] npm test 五案全過
  4. [x] rules/terminal/Emulator UI 留證

上一篇
[Day 6] 資料要活過隔天,先回答「這是誰的」
系列文
DishFlow AI Agent:用 Google AI 打造 Eat-Cost Balance 的下一餐決策系統8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言