iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
JavaScript

從看不懂到做出來,用 PawPal 走過前端新手村系列 第 14

Day 14|權限控管:不是每個人都能做每件事。

  • 分享至 

  • xImage
  •  

今天的故事

前面幾天,我開始理解登入、Token,以及後端怎麼確認目前的使用者是誰。

但接著我想到另一個問題:

使用者登入後,就代表他可以操作系統裡的所有資料嗎?

答案當然不是。

當時我最在意的,不是按鈕能不能按,而是不同帳號之間的資料能不能真正被隔開。

以 PawPal 來說,每一位使用者都有自己的會員資料、寵物資料,以及和自己帳號或寵物相關的私有紀錄。

如果一個帳號可以看到另一個帳號的資料,甚至修改或刪除對方的內容,就不只是功能做錯,而是直接牽涉到使用者隱私。

所以我開始思考:

我要怎麼確保每個帳號,只能看到和操作屬於自己的資料?

這也是我第一次真正開始理解「權限控管」。


有登入,不代表什麼都能做

一開始,我很容易把「登入驗證」和「權限控管」想成同一件事。

只要使用者登入成功,系統已經知道他是合法會員,我就會直覺覺得後面的功能應該都可以繼續執行。

但後來我才理解,這兩件事其實不一樣。

Authentication
→ 確認你是誰

Authorization
→ 確認你可以看什麼、做什麼

登入驗證只能確認這位使用者的身分。

但就算身分已經確認,也不代表他可以查看、修改或刪除所有人的資料。

例如:

使用者 A 已經登入
→ 可以取得使用者 A 的寵物資料

但不代表
→ 可以取得使用者 B 的寵物資料

而 PawPal 的權限控管,也不是以 admin、member 這種角色權限為主。

它比較接近:

ownership-based authorization

也就是:

這筆資料是不是屬於目前登入的會員?

所以 Day 13 解決的是:

你是誰?

到了 Day 14,我開始面對的問題則是:

知道你是誰之後,你到底可以操作哪些資料?


我最在意的是資料隱私

當時我想到的情況其實很多。

例如:

  • 使用者會不會修改到別人的寵物?
  • 使用者會不會刪除到別人的資料?
  • 使用者會不會看到其他會員的個人資訊?
  • 如果知道某筆資料的 ID,能不能直接呼叫 API?

但其中最讓我在意的還是:

我不能看到其他帳號的私有資料。

如果後端已經把不屬於這位會員的資料回傳出去,就算前端最後沒有把它顯示在畫面上,那些資料其實還是已經離開後端了。

所以比起:

前端拿到全部資料
→ 再自己篩選

更合理的做法應該是:

確認目前登入者
→ 後端直接限制查詢範圍
→ 只取得屬於目前會員的資料

也就是:

不該看到的私有資料,一開始就不應該被回傳。


不能只靠前端把按鈕藏起來

前端當然可以根據目前使用者的狀態決定哪些按鈕要顯示。

例如不是自己的資料時,不顯示編輯或刪除按鈕。

PawPal 的評論功能裡也有類似的做法。

當評論屬於目前登入者時,畫面才會出現自己的編輯、刪除操作。

這樣可以改善使用體驗。

但我後來才理解:

畫面沒有按鈕,不代表後端真的禁止這個操作。

因為 API 並不只能透過畫面上的按鈕呼叫。

如果真正的權限限制只做在前端,使用者還是有可能直接對 API 發出 Request。

所以兩邊的責任其實不同:

前端
→ 決定畫面要提供哪些操作入口

後端
→ 真正決定這次資料操作能不能成功

前端可以讓操作流程合理。

但真正保護資料的限制,還是要放在後端。


讀取資料時,就先限制範圍

以 PawPal 的寵物清單為例。

如果我要取得目前會員的寵物,後端不是先查出整個 pets 資料表,再交給前端過濾。

而是直接根據目前登入者限制查詢範圍。

概念上可以先理解成:

SELECT *
FROM pets
WHERE user_id = 目前登入者的 ID;

這裡是簡化示意。

PawPal 實際上會把 middleware 確認後的 req.userId 傳到後面的資料查詢流程,再用 user_id 限制可以取得的資料。

所以整個概念比較接近:

目前登入者
→ req.userId
→ 查詢 user_id 符合的 pets
→ 只回傳自己的寵物

這時我才真正發現:

Authorization 不一定是在資料查完之後才做。

有時候,權限本身就是查詢條件的一部分。


知道資料 ID,也不代表可以操作

一開始我原本以為,修改或刪除資料的權限判斷可能會是:

先找到資料
→ 查出這筆資料的 owner
→ 和目前登入者比較
→ 相同才允許操作

這是一種合理的作法。

但重新看 PawPal Repository 後,我才發現 pets 的主要實作方式不是這樣。

以修改寵物來說,概念更接近:

UPDATE pets
SET ...
WHERE id = 指定的寵物 ID
  AND user_id = 目前登入者 ID;

刪除也是:

DELETE FROM pets
WHERE id = 指定的寵物 ID
  AND user_id = 目前登入者 ID;

也就是 SQL 不只看:

這筆資料的 id 對不對?

還會一起確認:

這筆資料的 user_id
是不是目前登入者?

所以就算知道別人的寵物 ID,只要 user_id 不符合目前登入者,這筆資料一樣不會被修改或刪除。

這讓我第一次很具體地看到:

權限控管其實可以直接存在 SQL 的條件裡。

不是一定要先把資料查出來,再另外寫一段 JavaScript 比對 owner。


為什麼找不到資料,而不是直接回 403?

我以前會直覺覺得:

沒有權限
→ 403 Forbidden

但重新看 PawPal 實作後,我才發現事情沒有這麼固定。

以 pets 來說:

WHERE id = petId
AND user_id = req.userId

如果資料不存在,當然查不到。

但如果資料真的存在,只是它屬於另一位會員,因為 user_id 不符合,一樣查不到。

所以最後都可能得到:

404 找不到寵物

也就是對這個登入者而言:

沒有一筆符合「這個 ID,而且是你的」的資料。

這也讓我理解,權限錯誤並不是所有情況都一定使用相同的 HTTP Status。

比起死記:

Authorization = 403

我現在更在意的是:

後端是否真的阻止了不該成功的操作?


middleware 並沒有把所有權限都做完

前一篇已經介紹過 PawPal 的 authenticateToken

到了這篇,我比較在意的是它和 Authorization 到底怎麼分工。

整個流程可以簡化成:

Request
→ authenticateToken
→ 確認登入者
→ req.userId
→ Controller
→ Service / SQL
→ 限制 ownership

authenticateToken 主要負責的是 Authentication。

它做完的事情比較像:

我已經知道這個 Request 是哪一位會員送出的。

但它不會直接判斷:

這隻寵物是不是你的?
這筆紀錄是不是你的?

真正的資料所有權限制,主要還是在後面的 Service 和 SQL。

例如:

WHERE user_id = req.userId

或:

WHERE id = petId
AND user_id = req.userId

這樣我才真正把兩件事分開:

Authentication
→ 確認目前使用者的身分

Authorization
→ 利用這個身分限制可以操作的資料

不是所有 API 都需要登入

剛開始理解受保護 API 時,我也差點走到另一個極端。

我會覺得:

只要和會員或評論有關,好像全部都應該要求登入。

但 PawPal 實際上還是會區分公開資料和會員私有資料。

例如自己的:

會員資料
寵物資料
寵物修改與刪除
自己的評論修改與刪除

這些操作就需要知道目前登入者是誰。

但像醫院的公開評論列表:

GET /api/v1/hospitals/:hospital_id/reviews

本來就是要讓使用者查看的公開內容,因此不需要登入。

所以真正要判斷的不是:

這支 API 有沒有碰到會員資料?

而是:

這次 Request 是否涉及會員私有資料,或只能由資料本人執行的操作?


/reviews/me 是另一個很直覺的例子

除了 pets,我覺得 PawPal 的醫院評論也很適合理解 ownership。

評論修改與刪除 API 使用:

/reviews/me

像是:

PATCH /api/v1/hospitals/:hospital_id/reviews/me
DELETE /api/v1/hospitals/:hospital_id/reviews/me

光從 /me 就很容易理解:

這次操作的是「我自己的評論」。

後端也會把:

hospital_id
+
user_id

一起當作條件。

所以同一間醫院可以有很多會員留下評論,但目前登入者只能修改或刪除自己的那一筆。

不過這篇我還是以 pets 當成主要案例。

因為從:

取得列表
取得單筆
新增
修改
刪除

都可以看到 ownership 的影響。

評論則讓我看到,同一個觀念也會出現在其他功能裡。


如果現在重新做一次

如果現在重新處理權限控管,我不會只確認:

這支 API 有沒有加 middleware?

而是會沿著整條資料流去看。

這支 API 是否需要登入
→ middleware 是否取得目前登入者
→ Controller 是否正確使用 req.userId
→ Service / SQL 是否限制資料 ownership
→ 查詢、修改、刪除是否都綁定目前登入者
→ 沒有符合資料時,操作是否確實停止

因為只完成其中一層,還是不夠。

只做 Authentication:

知道你是誰

不代表已經確認:

這筆資料是你的

只在前端隱藏按鈕:

使用者比較不容易點到

也不代表:

後端真的拒絕這次 Request

而如果 SQL 只有:

WHERE id = petId

它只回答:

這筆資料存在嗎?

加入:

AND user_id = req.userId

才多回答了一個非常重要的問題:

這筆資料也是你的嗎?


本篇重點與學習心得

這次我真正理解的是:

  1. 有登入,不代表可以操作所有資料。
  2. Authentication 是確認身分,Authorization 是限制可以操作的內容。
  3. PawPal 主要採用的是 ownership-based authorization,而不是角色型 RBAC。
  4. 前端隱藏操作入口不能取代後端權限限制。
  5. 私有資料應該從後端查詢時就限制範圍。
  6. PawPal 的 pets 會依操作情境,把目前登入者的 user_id 納入查詢條件;修改或刪除特定資料時,也會和資料 ID 一起限制。
  7. authenticateToken 主要負責 Authentication,ownership 則主要由後續 Service / SQL 限制。
  8. 沒有權限不一定全部都回 403,但不該成功的操作一定不能成功。
  9. 公開資料與會員私有資料,要分開決定是否需要登入。

這次最讓我有感的,是權限控管並不是「把某個按鈕鎖起來」。

真正保護的是:

這筆資料只能被正確的人取得和操作。

以前我可能只會問:

這個功能有沒有成功?

開始碰到會員資料後,我才多了一個習慣:

這筆資料是誰的?

以及:

現在這個使用者,真的有資格操作它嗎?

而重新回頭看 Repository 後,我又多理解了一件事。

有時候權限並不會出現在一個叫做:

checkPermission()

的函式裡。

它可能只是 SQL 裡的一個:

AND user_id = req.userId

看起來只是多一個條件。

但真正保護的,是不同帳號之間的資料邊界。

從這裡開始,我才慢慢理解:

功能可以正常執行還不夠,它還必須只對正確的人執行。


下一篇預告

功能完成後,真正讓我頭痛的,往往不是畫面做不出來。

而是它明明看起來應該要能動,實際上卻一直出錯。

有時候資料沒有更新,有時候畫面完全沒有反應,也有時候錯誤訊息已經出現在眼前,我卻還是不知道問題到底在哪裡。

下一篇,我會從 PawPal 的真實錯誤案例出發,整理自己怎麼透過 console、錯誤訊息和排查流程,一步一步找到問題。

下一篇:

Day 15|Debug 到懷疑人生的那一天。


上一篇
Day 13|面試被問到 JWT,我才發現自己沒有真的懂
系列文
從看不懂到做出來,用 PawPal 走過前端新手村14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言