iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 23 篇

Day 23|知道你是誰之後,還能做什麼?Authentication vs Authorization

  • 分享至 

  • xImage
  •  

前言

前幾天已經完成 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:你是誰?

Authentication 是身分驗證,負責確認目前的使用者是誰。

前面的 JWT Middleware 已經處理了這件事。Token 驗證成功後,使用者資訊會放進 req.user,例如:

req.user = {
  userId: 123
};

因此後面的 API 可以直接取得:

req.user.userId

知道目前登入者是 User 123。

到了這裡可以先把 Authentication 記成一句話:

Authentication 是確認「你是誰」。

Authorization:你可以做什麼?

Authorization 則是在使用者身分確認之後,進一步判斷他有沒有權限執行某個操作。

例如筆記系統裡,User A 可以查看、修改和刪除自己的 Note,但不能因為已經登入,就直接操作 User B 的資料。

所以一個 Request 進來之後,會依序遇到兩個問題:

Authentication
→ 你是誰?

Authorization
→ 你可以做什麼?

JWT Middleware 解決的是第一個問題,接下來則要根據 API 要操作的資料,判斷第二個問題。

Note Ownership:這筆資料是誰的?

在筆記系統裡,可以在 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 很常見的一種判斷方式,特別適合「每個使用者只能管理自己的資料」這類需求。

User A 不能操作 User B 的 Note

假設現在有一支修改 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 會操作某一筆特定資料,就需要確認目前使用者有沒有權限操作。

列表 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 vs 403

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 專案還很單純,先把這些基本分工做好就夠了。等系統變大,再把權限邏輯進一步集中管理會比較合適。

當使用者開始有不同 Role

前面的 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 有什麼不同?

當系統再複雜一點,只有 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 就不需要。

這樣做的目的,是把權限限制在必要範圍內。就算帳號或權限設定出了問題,也能盡量縮小影響範圍。

把 Authentication 和 Authorization 串起來

把前幾天的內容和今天放在一起看,整個流程就會很清楚。

使用者登入後取得 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,就能慢慢建立更完整的權限系統。


上一篇
Day 22|一次操作很多筆資料,怎麼確保不出錯?認識 Transaction
下一篇
Day 24|我的電腦可以跑,為什麼你的不行?Docker 入門
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言