MODULE 6 · PART 3 · 登入與驗證

登入了,但他看得到所有人的資料

上一課你裝好了保全,沒登入的人進不了 /dashboard。但小明登入後打開待辦清單,卻看到了小華、阿美、所有人的待辦全攤在眼前。問題出在哪?資料庫裡那一筆筆資料,根本不知道自己是「誰的」。這一課要做兩件事:讓每筆資料記得主人(Insert 帶上 user_id),以及讓每個人只撈得到自己的(user-scoped query)。然後我們會踩到一個 vibe coder 最常上線才爆的坑:光靠查詢過濾,其實擋不住有心人。

本課地圖(6-7 關聯用戶資料)
§1一個 collection,住了所有人的資料
§2資料要記得自己是誰的:user_id
§3Insert 時自動蓋上主人的名字
§4User-scoped Query:只撈自己的
§5只靠查詢過濾,擋不住有心人
§6設好 Security Rules:在資料庫層築牆
§1 · 這是什麼

一個 collection,住了所有人的資料

你的 todos collection(一堆待辦放在一起)現在是一個大通鋪,所有人的待辦混在一起。先看清楚問題長什麼樣。

概念資料歸屬 與 資料隔離

同一堆資料裝多個人的東西時,必須有辦法分辨哪筆是誰的,這叫 資料歸屬(ownership)。而讓每個人只碰得到屬於自己的那部分,叫 資料隔離(isolation)。沒有這兩件事,所有人的資料就是一鍋大通鋪。

📁 todos collectionfilter: OFF
idtaskuser_id
t_001買牛奶u_ming
t_002繳房租u_ming
t_003訂機票去日本u_hua
t_004還阿美 500 元u_hua
t_005健身房續約u_amei
§2 · 為什麼

資料要記得自己是誰的:user_id

解法的第一步:每筆資料多一個 user_id 欄位,記住那個登入的人是誰(他在 Firebase Auth 的 uid)。這就是「資料歸屬」。

概念user_id 是一條認親線索

user_id 這個欄位,存的就是登入者在 6-5 學過的 Firebase Authentication 裡的那個 uid。還記得 6-5 那張 JWT 裡的 sub 嗎?那個就是登入者的 uid。每筆 todo 存上它,就等於在資料上蓋了主人的名字。

📁 todos collectionfilter: OFF
idtask
t_001買牛奶
t_002繳房租
t_003訂機票去日本
t_004還阿美 500 元
t_005健身房續約

現在這些待辦是孤兒,每份 document 只有 id 和 task,沒人知道哪筆是誰的。打開上面的開關,幫它們認領主人。

🔑VIBE CODER 秘訣
👀觀察
你叫 AI「做一個待辦功能」,它很開心地生出能新增、能列出的 CRUD,本機自己一個帳號玩超順。但它常常沒在每筆資料加上 user_id 欄位,也沒在新增時帶上登入者。等到第二個使用者註冊,大家的待辦全混在同一個通鋪裡。
💬怎麼跟 AI 講
把「多使用者」這個前提講出來:「這個 App 會有很多使用者,每個人只能看到和操作自己的資料。請幫每筆資料加上 user_id 記住登入的使用者是誰,新增資料時自動帶入目前登入者。」你不用指定欄位名稱,但要把「資料是私有的、每個人一份」這個產品需求說清楚。
§3 · 怎麼運作(寫入)

Insert 時自動蓋上主人的名字

新增一筆待辦時,user_id 從哪來?從 Session User 自動帶入,不是讓使用者自己填。

概念Session User
6-5 登入後拿到的 Session,這裡第一次被「」起來:不只是證明你是誰,後端還會從這張 Session 取出登入者的 id,蓋在你新增的每筆資料上。目前登入的是 小明(u_ming
新增待辦
正確寫法長這樣(AI 會給你這種 code)
const user = auth.currentUser
await addDoc(collection(db, 'todos'), {
  task,
  user_id: user.uid,  // 來自登入狀態,不是前端表單
})
§4 · 怎麼運作(讀取)

User-scoped Query:只撈自己的

查詢時加一個 .where('user_id', '==', user.uid),小明就只看得到小明的。切身分試試看。

用誰的身分登入:
db.collection('todos').where('user_id', '==', 'u_ming')
📁 todos collectionviewer: 小明filter: ON
idtaskuser_id
t_001買牛奶u_ming
t_002繳房租u_ming

filter 開著,切換上面的身分看看:每個人只剩自己的待辦,別人的 row 直接消失。這就是 資料隔離成立的樣子。

📝想一下
小明登入後,你希望他只看到自己的待辦。下面哪個查詢做得到?
§5 · 最關鍵的一課

只靠查詢過濾,擋不住有心人

上一段的 .where() 看起來保護好了。但少寫一個過濾、或有人直接改請求參數,就能撈到別人的資料。

你現在是小明,前提是上一段的 .where('user_id') 看起來已經把資料保護好了。我們來戳兩個洞。

db.collection('todos').where('user_id', '==', 'u_ming')
📁 todos collectionviewer: 小明filter: ONRules: OFF
idtaskuser_id
t_001買牛奶u_ming
t_002繳房租u_ming

點上面任一顆攻擊按鈕,看看會發生什麼。

這兩個洞的共通點:防線寫在「應用層」,而應用層的程式碼可以被改、被繞過、被 AI 漏寫。只要防線在你能控制的那一側,攻擊者也能繞過它。真正的邊界,要設在資料庫本身。
📝想一下
你已經在每個查詢都加了 .where('user_id', '==', user.uid),看起來每個人都只看得到自己的資料。這樣資料就安全了嗎?
🔑VIBE CODER 秘訣
👀觀察
就算 AI 幫你加了 .where('user_id'),它十之八九忘了設 Security Rules。因為在它的 demo 裡「查詢有過濾」看起來就夠了、跑得動。但 Firestore 如果用「測試模式」開發,那條「先全部放行」的規則 30 天後就失效,很多人(和 AI)乾脆把它直接帶上線,等於資料庫大門沒鎖,任何拿到你 API 的人都能撈、能改別人的資料。這是上線才爆、最痛的一類洞。
💬怎麼跟 AI 講
把安全邊界當成驗收條件:「請幫這個 collection 設好 Firestore Security Rules,讓每個使用者只能讀寫自己的資料,並確認就算前端少寫過濾條件,資料庫層也擋得住。」收到方案後,自己用第二個帳號實際登入測一遍(多帳號測試),確認看不到對方的資料,才算數。
§6 · 對你的意義

設好 Security Rules:在資料庫層築牆

同一個攻擊,設好 Security Rules 再打一次,資料庫直接擋下。這才是上線該有的邊界。

概念Firestore Security Rules

Firebase 的功能,在資料庫層為每一份 document 設規則:「只有 request.auth.uid == resource.data.user_id 的人,才能讀或寫這一筆」。不管請求從哪來、查詢有沒有過濾,資料庫自己會擋。它信的是登入狀態裡的 request.auth.uid,不是前端說的那個 user_id。

🔥 Firebase
Authentication
Firestore Database
Storage
Rules
↳ todos
🔒Security Rules · todosDisabled
規則還沒設,資料庫大門沒鎖,誰問就給誰。打開它,套上一條 rule。
用小明的身分,重打一次剛才的攻擊:
📁 todos collectionviewer: 小明filter: OFFRules: OFF
idtaskuser_id
t_001買牛奶u_ming
t_002繳房租u_ming
t_003訂機票去日本u_hua← 別人的!
t_004還阿美 500 元u_hua← 別人的!
t_005健身房續約u_amei← 別人的!
⚠️ 小明 看到了不屬於自己的資料。

規則還沒設,攻擊得逞:漏寫 filter,全部人的資料攤開。 打開上面的 Rules 開關,再看一次。

進階補充:讀寫可以分開設,還有那個最常見的上線地雷

上面用 allow read, write 一次涵蓋了讀和寫。想分得更細,可以拆成 allow readallow write,寫入時一樣用 request.auth.uid == request.resource.data.user_id 確保你只能新增 / 改自己的那幾筆。

最關鍵一句:Firestore 剛建立時若選「測試模式」,規則是「先全部放行」、但 30 天後會自動失效;若選「正式模式」則預設「全部拒絕」。最常見地雷就是把測試模式那條全開規則直接帶上線,等於資料庫大門整個敞開。上線前一定要逐個 collection 確認規則有設對。

📝想一下
Security Rules 設好了,資料庫已經會擋。那 app 層查詢裡的 .where('user_id', '==', user.uid) 還要不要寫?
收尾

三部曲收齊了

同一個 collection 裝多人的資料,每筆都要有 user_id 記得自己是誰的(資料歸屬)。
Insert 時 user_id 從 session user 自動帶入,絕不信任前端傳來的 user_id。
讀取時用 .where('user_id', '==', user.uid) 做 user-scoped query,達成資料隔離。
但 app 層過濾擋不住竄改與漏寫,真正的邊界是資料庫層的 Firestore Security Rules。
Firestore 測試模式的規則 30 天後會失效、正式模式預設全拒,上線前務必把 Security Rules 設對,並用第二個帳號實測。
第六章「登入與驗證」總收尾

這一章你親手把一個「誰都能進、誰的資料都看得到」的玩具,變成了一個有門禁的真產品:

6-5解決「你是誰」:用 Google 登入拿到身分。
6-6解決「你能不能進這扇門」:Middleware 擋住未登入的人。
6-7解決「進來後你只能碰自己的東西」:user_id 關聯 + Security Rules 把資料鎖在主人手裡。

身分、路由、資料,三道門關齊,登入與驗證才算完整。這也是 vibe coder 跟 AI 協作時最該盯緊的一塊:AI 很會把功能寫到「能跑」,但「能不能安全地給真人上線」這條線,要你自己劃。