前幾天已經完成 JWT 登入流程,也透過 JWT Middleware 確認使用者身分。驗證成功後,後端可以從 req.user 取得目前登入者的資訊。
做到這裡,API 已經知道「你是誰」,但接著還會遇到另一個問題:知道你是誰之後,你可以操作哪些資料?
假設 User A 和 User B 都已經登入,User A 發送:
GET /api/notes/15
後端知道這個 Request 是 User A 發出的,但 15 號 Note 是誰的?如果它屬於 User B,User A 還能不能查看、修改或刪除?
登入解決的是身分問題,資料權限則需要另外判斷。這就是今天要認識的 Authentication 和 Authorization。
Authentication 是身分驗證,負責確認目前的使用者是誰。
前面的 JWT Middleware 已經處理了這件事。Token 驗證成功後,使用者資訊會放進 req.user,例如:
req.user = {
userId: 123
};
因此後面的 API 可以直接取得:
req.user.userId
知道目前登入者是 User 123。
到了這裡可以先把 Authentication 記成一句話:
Authentication 是確認「你是誰」。
Authorization 則是在使用者身分確認之後,進一步判斷他有沒有權限執行某個操作。
例如筆記系統裡,User A 可以查看、修改和刪除自己的 Note,但不能因為已經登入,就直接操作 User B 的資料。
所以一個 Request 進來之後,會依序遇到兩個問題:
Authentication
→ 你是誰?
Authorization
→ 你可以做什麼?
JWT Middleware 解決的是第一個問題,接下來則要根據 API 要操作的資料,判斷第二個問題。
在筆記系統裡,可以在 notes 資料表記錄每筆 Note 的擁有者:
notes
id | title | user_id
---|-------------|--------
1 | Node.js | 123
2 | JWT | 123
3 | PostgreSQL | 456
這表示 Note 1 和 Note 2 屬於 User 123,Note 3 屬於 User 456。
當 User 123 要操作 Note 1 時:
登入者 userId → 123
Note userId → 123
兩邊相同,就代表目前使用者擁有這筆資料。反過來,如果 User 123 想操作 Note 3,兩個 userId 不相同,就代表這筆資料屬於其他使用者。
這個「資料屬於誰」的關係,就是 ownership。它是 Authorization 很常見的一種判斷方式,特別適合「每個使用者只能管理自己的資料」這類需求。
假設現在有一支修改 Note 的 API:
PATCH /api/notes/3
User A 登入後,req.user.userId 是 123,查詢資料庫後發現 Note 3 的 userId 是 456。雖然 User A 已經通過登入驗證,但這筆資料不是他的,所以不能繼續修改。
可以在 Controller 或 Service 裡做檢查:
async function updateNote(req, res, next) {
try {
const noteId = Number(req.params.id);
const userId = req.user.userId;
const note = await noteRepository.findOneBy({
id: noteId
});
if (!note) {
return res.status(404).json({
message: 'Note not found'
});
}
if (note.userId !== userId) {
return res.status(403).json({
message: 'Forbidden'
});
}
note.title = req.body.title;
note.content = req.body.content;
const updatedNote = await noteRepository.save(note);
return res.status(200).json({
data: updatedNote
});
} catch (error) {
next(error);
}
}
這裡最重要的是 note.userId 和 req.user.userId 的比較。只有確認這筆 Note 屬於目前登入者,才會繼續執行更新。
同樣的概念也適用在:
GET /api/notes/:id
PATCH /api/notes/:id
DELETE /api/notes/:id
只要 API 會操作某一筆特定資料,就需要確認目前使用者有沒有權限操作。
單筆資料有做 ownership 檢查,列表 API 也不能忽略。
假設 User A 發送:
GET /api/notes
如果後端直接查詢:
const notes = await noteRepository.find();
就可能把 User A、User B、User C 的所有 Note 一起查出來。即使單筆 GET /api/notes/:id 已經做了權限檢查,User A 還是可能在列表裡直接看到其他人的資料。
因此列表 API 也需要把目前登入者的 userId 放進查詢條件:
async function getNotes(req, res, next) {
try {
const userId = req.user.userId;
const notes = await noteRepository.find({
where: {
userId
}
});
return res.status(200).json({
data: notes
});
} catch (error) {
next(error);
}
}
這樣 User A 查詢時,資料庫就只會回傳 userId = 123 的 Note。
因此整組 Note API 都會有自己的資料範圍:
GET /api/notes
→ 只能取得自己的 Note
GET /api/notes/:id
→ 只能查看自己的 Note
PATCH /api/notes/:id
→ 只能修改自己的 Note
DELETE /api/notes/:id
→ 只能刪除自己的 Note
可以看到,權限檢查有時候會是在查詢結果出來之後比較,有時候則可以直接放進資料庫查詢條件。重點是 API 最後不能把使用者沒有權限取得的資料送出去。
看到這裡,可能會想到另一個問題:如果前端已經知道這個 Note 不是 User A 的,那把編輯和刪除按鈕藏起來,不就好了嗎?
前端可以這樣做,但這只能改善畫面上的操作體驗,不能當成真正的權限控制。
因為使用者可以直接操作瀏覽器的開發者工具,甚至自己寫程式送出 Request,例如:
PATCH /api/notes/3
所以即使前端完全沒有顯示「編輯」按鈕,惡意使用者還是可以直接呼叫 API。
真正的權限檢查一定要放在後端。後端收到 Request 後,還是要自己確認 userId、Role 或其他權限條件,不能直接相信前端傳來的資料。
可以把前後端的分工想成:
Frontend
→ 控制畫面上顯示哪些功能
Backend
→ 真正決定能不能執行
前端可以先擋掉不必要的操作,但最後的決定權還是在後端。
401 和 403 可以直接從 Authentication 和 Authorization 的差別來理解。
401 Unauthorized 是身分驗證沒有通過,例如沒有帶 Token、Token 無效或 Token 已經過期。這時候後端還不能把這個 Request 視為有效的登入使用者。
403 Forbidden 則是使用者已經通過身分驗證,後端知道他是誰,但他沒有權限執行這個操作。
例如:
沒有有效 Token
→ Authentication 失敗
→ 401
Token 有效
→ 確認是 User A
→ 但 Note 屬於 User B
→ Authorization 失敗
→ 403
所以可以簡單記成:
401 → 身分驗證有問題
403 → 身分確認成功,但權限不足
User A 修改 User B 的 Note 時,就屬於第二種情況。
JWT Middleware 已經存在,那 ownership 檢查是不是也應該全部放進 Middleware?
通常會把兩件事情分開。JWT Middleware 負責共通的身分驗證,確認 Token、取得使用者資訊,再把資料放進 req.user;Controller 或 Service 則根據目前 API 要操作的資料,進一步做 ownership 或其他權限判斷。
整體流程可以想成:
Request
↓
JWT Middleware
↓
確認 Token
↓
取得 userId
↓
req.user
↓
Controller / Service
↓
查詢資料
↓
確認權限
↓
允許或拒絕操作
這樣分工會比較清楚,Middleware 不需要知道每一支 API 要處理的是 Note、Comment 還是 Order,只要負責確認使用者身分;實際要不要讓這個使用者操作某筆資料,再交給處理資料的程式碼。
如果同一套權限判斷在很多地方都會出現,也可以再抽成共用的 function,例如:
function canAccessNote(note, user) {
return note.userId === user.userId;
}
之後不同 API 就可以共用這個判斷,避免相同的 ownership 邏輯散落在各個地方。
目前的 Note 專案還很單純,先把這些基本分工做好就夠了。等系統變大,再把權限邏輯進一步集中管理會比較合適。
前面的 ownership 解決的是「這筆資料是不是你的」,但實際系統裡,使用者之間也可能有不同角色。
例如:
Admin
→ 可以管理所有使用者和 Note
Editor
→ 可以新增、修改文章
Member
→ 可以管理自己的 Note
這時候就可以使用 Role-Based Access Control,也就是 RBAC。它的概念很簡單:先定義不同 Role 可以做哪些事情,再把使用者放進對應的 Role。
例如:
User A → Member
User B → Admin
API 就可以根據 Role 判斷使用者能不能執行某些操作。
不過 Role 和 ownership 處理的事情不太一樣:
Role
→ 你是哪一種使用者?
Ownership
→ 這筆資料是不是你的?
兩者也可以一起判斷。
例如:
Admin
→ 可以修改所有 Note
Member
→ 只能修改自己的 Note
Member 可能擁有 note:update 的權限,但真正修改時,還要再檢查 Note 是不是自己的。所以實際的權限規則可能會同時包含 Role 和 Ownership。
當系統再複雜一點,只有 Role 可能會開始不夠細。這時候可以把具體能做的事情拆成 Permission,例如:
note:read
note:create
note:update
note:delete
user:read
user:update
user:delete
這時候可以這樣理解:
Role
→ 你是什麼角色?
Permission
→ 你具體可以做什麼?
例如:
Admin
→ note:read
→ note:create
→ note:update
→ note:delete
→ user:read
→ user:update
→ user:delete
Member
→ note:read
→ note:create
→ note:update
→ note:delete
即使 Member 擁有 note:update,實際操作時還是可能需要搭配 ownership,只能修改自己的 Note。
所以一個稍微完整一點的權限判斷可能同時包含:
Role
+
Permission
+
Ownership
目前的 Note 專案還不需要一次把整套 Role、Permission 都做出來,先知道這些概念之間怎麼配合就已經很夠了。
權限設計還有一個很重要的原則,就是最小權限。
意思是使用者只需要拿到完成工作所需要的權限,不需要的權限就不要給。
例如 Member 需要修改自己的 Note,那麼可以讓他擁有:
note:read
note:create
note:update
note:delete
但不需要讓他直接擁有:
user:delete
Admin 可能需要管理所有使用者,Member 就不需要。
這樣做的目的,是把權限限制在必要範圍內。就算帳號或權限設定出了問題,也能盡量縮小影響範圍。
把前幾天的內容和今天放在一起看,整個流程就會很清楚。
使用者登入後取得 JWT,之後帶著 JWT 呼叫需要登入的 API。JWT Middleware 先確認使用者身分,成功後把資訊放進 req.user;接著 API 再根據目前要操作的資料,以及 Role、Permission 或 Ownership 等規則,確認這個使用者能不能繼續操作。
整個流程可以整理成:
Request
↓
JWT Middleware
↓
Authentication
↓
確認使用者是誰
↓
取得 req.user
↓
Authorization
↓
檢查 Role / Permission / Ownership
↓
允許或拒絕操作
↓
存取資料
列表 API 則可以直接把權限條件放進查詢,從一開始就限制資料範圍。
所以「已經登入」只是第一層。真正碰到資料時,還需要再檢查權限,這樣才能避免 User A 因為知道一個 Note ID,就直接讀取或修改 User B 的資料。
前面幾天學 JWT 時,主要是在解決「系統怎麼知道使用者是誰」。到了今天,又多了一層判斷,知道你是誰之後,還要確認你能不能操作眼前這筆資料。
Authentication 負責身分,Authorization 負責權限。Ownership 可以限制使用者只能操作自己的資料,Role 可以區分不同類型的使用者,Permission 則可以把具體能做的事情拆得更細,這些方式可以依照系統需求組合使用。
對現在的 Note API 來說,先做好 Authentication 和 Ownership 已經能解決很基本的資料權限問題。等系統開始出現 Admin、Editor、Member 等角色,再進一步加入 Role 和 Permission,就能慢慢建立更完整的權限系統。