在 PawPal 開發寵物功能時,我有參與 JWT 驗證相關功能的實作。
當時我知道 JWT 和登入有關,也知道 API 需要帶上 Token。
只要登入成功、Token 有保存,受保護的 API 能正常呼叫,我就會覺得這個功能已經完成了。
但那時候的我,其實還沒有真正理解整個流程。
我不知道 Token 是從哪裡產生的,也說不清楚:
直到後來面試時,我被問到一個問題:
JWT 有沒有期限?
我當下知道 Token 好像可以設定期限,卻沒辦法把 PawPal 的實作完整說清楚。
那次經驗讓我發現:
功能曾經做過,不代表我真的理解它。
所以我重新回到 PawPal 的程式碼,把 JWT 從登入、傳送、驗證,到取得會員身分的流程重新走了一次。
面試官問我「JWT 有沒有期限?」時,我第一個反應是:應該有,而且好像可以在產生 Token 時設定。
但如果繼續問下去,我當時其實回答不出來:
我曾經參與過相關功能,也看過 Token 出現在登入流程裡,但我的理解主要停留在「照著現有流程使用」。
當問題從「功能怎麼操作」變成「為什麼這樣設計」時,我才發現自己沒有真正把程式碼串起來理解。
這也是我後來重新打開 PawPal 程式碼的原因。
一開始接觸 JWT 時,我只知道它像登入後取得的一張通行證。
登入成功後,後端會回傳 Token;前端把它保存起來,之後呼叫需要登入的 API 時再附上。
但當時我的理解大概停在:
登入成功
↓
取得 Token
↓
帶著 Token 呼叫 API
至於 Token 裡面有什麼、後端怎麼確認真假,以及驗證完成後怎麼知道目前是誰,我沒有真正拆開來看。
這也是為什麼面試官問到期限時,我雖然做過相關功能,卻沒有辦法有條理地回答。
重新查看程式碼後,我才發現 JWT 並不是只有「產生一串字串」。
它連接了登入、會員身分與 API 保護,也成為後續進行權限判斷時的重要依據。
使用者輸入帳號密碼並登入成功,只能證明那一次登入請求通過了。
接下來,使用者還會:
HTTP 請求本身不會自動記得上一個請求是誰。
所以使用者每次呼叫受保護的 API 時,都需要帶上一個可以代表身分的資訊。
在 PawPal 裡,這個資訊就是 JWT。
可以先把它想成:
登入成功後取得的身分通行證
前端之後每次呼叫受保護的 API,都把這張通行證一起送給後端。
後端驗證通過後,才知道這次請求來自哪一位會員。
PawPal 在登入成功後,會使用 jwt.sign() 產生 Token。
下面是簡化後的概念:
const token = jwt.sign(
{
sub: user.id,
email: user.email,
},
process.env.JWT_SECRET,
{
expiresIn: '7d',
},
)
這裡有三個重要部分。
第一個是 Payload:
{
sub: user.id,
email: user.email,
}
sub 可以理解為這張 Token 代表的主體。
在 PawPal 裡,它保存的是會員 ID。
第二個是 Secret:
process.env.JWT_SECRET
它由後端保存,用來簽發及驗證 Token。
實際內容不能寫進前端、文章或 GitHub。
第三個是有效期限:
expiresIn: '7d'
PawPal 的 Token 設定為七天後到期。
如果 JWT 沒有設定期限,登入功能仍可正常運作,只是 Token 不會因為時間到了而自動失效。
取得 Token 後,前端在呼叫受保護的 API 時,需要把它放進 Request Header。
常見格式是:
Authorization: Bearer <token>
下面是簡化概念:
const headers = {
Authorization: `Bearer ${token}`,
}
接著呼叫 API:
await axios.get('/api/v1/pets', {
headers,
})
Bearer 可以先理解為一種 Authorization Header 的格式。
它告訴後端:
這次請求帶了一個代表使用者身分的 Token。
但前端有附上 Token,不代表後端就會直接相信它。
後端仍然必須驗證這張 Token 是否真的由系統簽發,以及是否仍在有效期限內。
PawPal 使用 Express Middleware 處理 JWT 驗證。
Middleware 可以先想成 API 前面的檢查站。
請求進入真正的功能以前,會先經過驗證:
前端送出 Request
↓
JWT Middleware
↓
驗證 Token
↓
驗證成功才進入 Controller
下面是簡化後的概念:
const payload = jwt.verify(token, process.env.JWT_SECRET)
req.userId = Number(payload.sub)
next()
jwt.verify() 會確認:
驗證失敗或 Token 已過期時,後端會回傳 401。
驗證成功後,Middleware 會再取得 payload.sub,確認會員 ID 後放進:
req.userId
最後呼叫:
next()
讓請求繼續進入下一個 Middleware 或 Controller。
Token 產生時,PawPal 已經把會員 ID 放進 sub:
{
sub: user.id,
}
Middleware 驗證成功後,就能讀取:
payload.sub
再將它轉成會員 ID:
req.userId = Number(payload.sub)
後面的 Controller 或 Service 就不需要再請前端傳一次會員 ID。
整個流程可以整理成:
登入成功
↓
Token 的 sub 保存會員 ID
↓
前端傳送 Bearer Token
↓
Middleware 驗證 Token
↓
讀取 payload.sub
↓
寫入 req.userId
↓
後端知道目前會員是誰
這時我才真正理解,JWT 不只是判斷「有沒有登入」。
它還讓後端能從已驗證的 Token 中取得目前會員身分。
JWT 驗證成功後,後端已經知道目前登入的會員是誰。
但這時還有另一個問題:
知道你是誰,不代表你可以操作所有資料。
JWT 主要處理的是 Authentication,也就是身分驗證。
至於修改或刪除資料時,後端如何確認這筆資料真的屬於目前會員,則屬於 Authorization,也就是權限判斷。
這部分會留到 Day 14 繼續整理。
重新整理 JWT 流程後,我終於比較能回答面試官當時問的問題。
JWT 可以設定期限。
在 PawPal 裡,Token 的期限是七天:
expiresIn: '7d'
在 PawPal 目前的流程中,期限到了之後,後端使用 jwt.verify() 驗證時就會失敗並回傳 401;要繼續使用受保護功能,就需要重新取得有效的 Token。
我也開始能把完整流程說清楚:
登入成功
↓
後端簽發 JWT
↓
前端保存並傳送 Token
↓
Middleware 驗證 Token
↓
從 payload.sub 取得會員 ID
↓
保存到 req.userId
↓
後端知道目前會員是誰
這次最大的收穫,不是多記住一個 jwt.verify()。
而是我開始知道,面對曾經做過的功能,不能只停在「它可以動」。
我需要能說明:
如果現在面試官再問我「JWT 有沒有期限?」,我會回答:
JWT 可以設定有效期限,但不是每一個 JWT 都一定會自動有期限。期限通常在後端使用
jwt.sign()產生 Token 時設定。PawPal 使用expiresIn: '7d',所以 Token 七天後會失效。後端使用jwt.verify()驗證時,如果 Token 已經過期,就會驗證失敗並回傳 401。
我也會補充,沒有設定期限的 Token 仍可正常產生與使用,只是不會因為時間經過而自動失效。
以前我只能回答「JWT 跟登入有關」。
現在我至少可以從產生、傳送、驗證到過期,把整個流程說清楚。
這也是這次重新整理程式碼後,對我最實際的幫助。
這次重新理解 JWT,我整理出六個重點:
sub,並設定七天期限。jwt.verify() 驗證,並把會員 ID 放進 req.userId。以前的我會說:
JWT 就是登入後拿到的 Token。
現在我會說:
JWT 讓後端驗證這次請求的身分,並從 Token 裡取得目前會員的 ID,讓後續 API 知道正在處理誰的請求。
這兩句話的差別,就是我從「做過功能」走到「開始真正理解功能」的過程。
JWT 驗證成功後,後端已經知道目前登入的人是誰。
但登入成功,不代表可以修改或刪除所有人的資料。
Day 14,我會繼續整理身分驗證與權限控管的差別,以及 PawPal 如何限制會員只能操作屬於自己的資料。
下一篇:
Day 14|權限控管:不是每個人都能做每件事。