做後台系統時,很常會遇到這種需求:
只有某些角色可以編輯
只有 Admin 可以刪除
沒有權限的人看不到某個按鈕
前端第一個反應通常會是:
{canEdit && (
<Button>Edit</Button>
)}
看起來很合理。
沒有權限:
看不到按鈕
有權限:
看得到按鈕
但如果做到這裡就覺得:
權限控管完成了。
其實還不夠。
因為:
UI 上看不到,不代表 API 真的不能被呼叫。
例如:
const canEdit = role === "ADMIN";
return (
<>
{canEdit && (
<Button>Edit</Button>
)}
</>
);
這段程式做的是:
role 是 ADMIN?
↓
Yes
→ 顯示 Edit Button
No
→ 不 Render Button
這是一種:
Frontend Permission
它控制的是:
使用者在 UI 上可以看到什麼、操作什麼。
假設使用者沒有編輯權限。
如果畫面還是顯示:
Edit
Delete
Approve
結果點下去全部:
403 Forbidden
使用者體驗會很差。
所以前端會根據權限:
隱藏按鈕
disable 欄位
隱藏頁籤
禁止進入某些頁面
這些都是合理的。
但它們主要是在處理:
UI / UX
不是完整的 Security Boundary。
因為前端程式是在使用者的瀏覽器裡執行。
使用者不一定只能透過:
你畫面上的 Button
呼叫 API。
例如:
Frontend
↓
PATCH /users/123
即使畫面上沒有:
Edit Button
只要有人知道這支 API,
理論上仍然可以嘗試:
直接送 HTTP Request
例如透過:
DevTools
API Client
其他程式
所以:
把 Button 藏起來,只是拿掉操作入口,不是拿掉 API 能力。
假設 API:
PATCH /orders/123
Backend 收到 Request 後,
不能只相信:
Frontend 既然送來了,代表這個人一定有權限。
而是還要自己確認:
這個 Request 是誰送的?
他的角色是什麼?
他可以修改這筆資料嗎?
例如概念上:
Request
↓
Authentication
↓
知道你是誰
↓
Authorization
↓
確認你可不可以做這件事
↓
允許 / 拒絕
這兩個詞很像,
我以前也很容易混。
可以先這樣記:
Authentication
→ 你是誰?
Authorization
→ 你可以做什麼?
例如:
登入 Google
↓
系統知道:
你是 user@example.com
這比較接近:
Authentication
接著系統判斷:
這個人是 Admin?
Editor?
Viewer?
再決定:
可不可以 Edit
可不可以 Delete
可不可以 Approve
這才是:
Authorization
例如編輯功能:
Frontend
↓
canEdit = true
↓
顯示 Edit Button
↓
使用者點擊
↓
PATCH Request
↓
Backend 再檢查權限
↓
允許更新
如果使用者沒有權限:
Frontend
↓
不顯示 Button
就算他真的想辦法自己送:
PATCH Request
Backend 仍然應該:
拒絕
這才比較完整。
可以先這樣分:
負責:
使用者看得到什麼?
哪些操作要隱藏?
哪些欄位 disabled?
哪些頁面不該出現在 UI?
主要目的:
UX
+
避免誤操作
負責:
這個 Request 到底允不允許?
主要目的:
Security
+
資料保護
所以它們不是:
二選一
而是:
兩層都要有
例如:
{isAdmin && (
<Button onClick={deleteUser}>
Delete
</Button>
)}
如果 Backend 的:
DELETE /users/123
完全沒有驗證使用者權限,
那真正的限制就只存在於:
React UI
這種保護很脆弱。
因為:
API 本身還是接受任何已知 Request
所以不能把:
Frontend condition
當成真正安全控制。
例如:
if (!canView) {
return <Forbidden />;
}
這可以阻止一般使用者透過 UI 看到頁面。
但如果頁面背後有:
GET /admin/report
Backend 一樣要檢查。
不能因為:
前端沒有 Route
就假設:
API 是安全的。
例如前端可能拿到:
{
canView: true,
canEdit: false,
canDelete: false
}
Component 再根據:
canEdit
決定:
Button 顯示與否。
所以前端資料流可能是:
User
↓
Role / Permission
↓
Frontend State / Context
↓
Component
↓
UI
這也是為什麼大型專案裡,
權限常常不會每個頁面自己寫:
role === "ADMIN"
而是抽成比較統一的:
Permission
Access
canEdit
canView
例如:
role === "ADMIN"
今天看起來沒問題。
但需求可能變成:
Admin 可以編輯
Manager 在自己部門也可以編輯
Owner 可以編輯自己建立的資料
這時候:
role === "ADMIN"
就不夠了。
真正的問題其實是:
這個人「能不能 Edit」?
所以:
canEdit
通常比:
isAdmin
更貼近 Component 真正需要知道的事情。
Component 可能根本不需要知道:
你是 Admin
Manager
Owner
它只需要知道:
<Button disabled={!canEdit}>
也就是:
你告訴我能不能 Edit 就好。
至於:
canEdit 是怎麼算出來的
可以留在更上層的 Permission Logic。
canEdit 也不能成為 Backend 的依據例如:
canEdit = true;
這只是前端目前的狀態。
不能送:
{
"canEdit": true
}
然後 Backend 就相信:
好,你說你可以編輯,那你就可以。
真正的 Backend Authorization,
應該根據:
經過驗證的使用者身份
+
Server 端權限規則
+
目前資源
重新判斷。
Client 可以傳:
我要修改哪一筆資料
我要做什麼操作
但不應該讓 Client 自己決定:
我有沒有權限。
因為 Client 資料可以被修改。
這也是一個很重要的安全觀念:
不要把 Client 當成可信任環境。
要。
因為 Backend 不知道 Request 是不是一定從正常 UI 來的。
如果使用者沒有權限,
API 應該明確拒絕。
常見可能看到:
401 Unauthorized
403 Forbidden
兩個名字很容易混。
可以先簡單理解:
401
→ 身份驗證有問題
→ 例如還沒登入 / Token 無效
403
→ 我知道你是誰
→ 但你沒有權限做這件事
實際狀態碼仍然要看 API 設計。
假設:
使用者看不到 Edit Button。
我會先問:
Frontend 的 canEdit 是多少?
↓
Permission 是怎麼算的?
↓
Button condition 是什麼?
這是:
UI 問題
但如果:
Button 看得到,點擊後 API 403。
那要再查:
使用者身份是誰?
↓
Backend 權限規則是什麼?
↓
這筆 Resource 是否允許這個人修改?
這是:
Authorization 問題。
這反而更值得注意。
因為:
Frontend 不讓操作
但:
Backend 卻允許
代表:
UI 看起來有權限控管,但真正的 Security Boundary 可能沒有做好。
這種問題不能只修:
Button condition
還要確認:
Server Authorization
是不是正確。
例如:
不能編輯
這句話其實太模糊。
可能是:
Button 不見
可能是:
Button disabled
可能是:
Route 進不去
可能是:
Request 403
可能是:
Backend Business Rule 拒絕
這些看起來都是:
沒權限。
但實際問題層級完全不同。
User Identity
↓
Authentication
↓
Permission / Role
↓
┌────────────────────┐
│ Frontend │
│ │
│ canView │
│ canEdit │
│ canDelete │
│ │
│ 控制 UI │
└────────────────────┘
↓
Request
↓
┌────────────────────┐
│ Backend │
│ │
│ Authorization │
│ │
│ 再次確認是否允許 │
└────────────────────┘
↓
Database
Frontend:
讓介面符合使用者權限。
Backend:
真正保護資源。
UI/UX 在意:
不要讓使用者看到不能做的操作
讓狀態清楚
減少挫折
Security 在意:
即使有人繞過 UI
也不能做沒有權限的事情
兩邊目標不同,
但其實應該一起合作。
隱藏 Button
≠
禁止 API
以及:
Authentication
→ 你是誰?
Authorization
→ 你能做什麼?
Frontend Permission 很重要,
但它主要控制:
UI / UX
真正的安全邊界,
還是必須由:
Backend
再檢查一次。
所以之後看到:
{canEdit && <Button />}
我不會只想:
有做權限了。
而會多問一句:
如果有人完全不經過這個 Button,直接打 API,Backend 也會拒絕嗎?
如果答案是:
Yes
那才比較接近完整的權限設計。
下一篇可以繼續把「安全邊界」往前推一步:
只是把 HTML Email Preview 顯示在 iframe 裡,為什麼還要加 sandbox?