上一篇做到註冊後自動登入,使用者已經可以直接進入會員頁面。
但接著我開始遇到另一個問題:
使用者雖然已經登入了,前端到底是怎麼知道現在登入的是誰?
當時我對會員流程的理解還很簡單。
我直覺認為,既然登入成功後已經拿到 Token,那前端應該就是靠 Token 去判斷目前的使用者。
直到開始處理個人資料與 Dashboard 的會員資訊時,我才慢慢發現,這件事其實比我原本想的多了一層。
在 PawPal 裡,登入成功時,後端其實不只回傳 Token,也會一起回傳基本的會員資料。
前端會把這些資料存進 auth store,畫面需要顯示會員姓名時,就可以直接從目前保存的 user 資料中取得。
但另一方面,當前端要去取得寵物資料或其他屬於目前會員的受保護資源時,又不是只靠畫面上保存的 user。
這時候還是需要帶著 Token 發出請求,讓後端驗證這次請求屬於哪一位登入者。
這也是我第一次開始理解:
「前端現在顯示的是誰」和「後端怎麼確認這次請求是誰送的」,其實是兩件相關、但不同的事情。
我一開始最大的誤解,是把登入想成:
登入成功
→ 取得 Token
→ 再透過 Token 取得會員資料
但 PawPal 實際上的登入流程不是只有這樣。
登入成功後,後端會回傳類似這樣的資訊:
token
+
user
其中 user 會包含像是:
id
name
email
avatar_url
created_at
前端收到資料後,會把 Token 和 user 都保存到 auth store,並同步保存到 localStorage。
例如:
pawpal_token
pawpal_user
所以登入完成後,前端其實已經擁有一份可以直接拿來顯示的基本會員資料。
像 Dashboard 上的會員名稱,就是直接從:
authStore.user
取得。
這和我原本想像的:
登入後前端完全不知道是誰,所以一定要再呼叫一次 Current User API。
其實不太一樣。
PawPal 的登入回應本身,就已經先把基本的 user 資料交給前端了。
後來查回 Repository,我才確認 PawPal 後端其實真的有一支 Current User API:
GET /api/v1/users/me
它的用途是:
前端帶著 Bearer Token 發出請求
→ 後端驗證 Token
→ 從 JWT payload 取得目前使用者 id
→ middleware 將它放進 req.userId
→ 依照 req.userId 查詢會員資料
→ 回傳目前登入者的 user 資料
簡單來說,這支 API 解決的是:
後端如何根據這次請求的登入身分,找出目前這位會員的資料?
但這裡有一個我後來才釐清的地方。
雖然 /users/me 真的存在,PawPal 當時登入完成後,前端並不是每次都立刻再呼叫這支 API 取得會員資料。
因為登入 API 本身就已經回傳 user,前端也已經把它存進 auth store。
所以更接近實際情況的理解應該是:
登入成功
→ Login API 回傳 token + user
→ authStore 保存 token、user
→ 畫面直接使用 authStore.user
而當系統需要取得目前會員資料時,後端也提供 /users/me 這類受保護 API 來處理。
這時候我才慢慢把 Token 和 user 資料分開來看。
登入回傳的 user 比較像:
前端目前可以直接使用的會員基本資料。
而 Token 的角色則比較像:
當前端之後再向後端要求受保護資料時,用來證明這次請求代表哪一位登入者。
在 PawPal 裡,受保護的 Request 會帶上:
Authorization: Bearer <token>
後端的 authenticateToken middleware 驗證 Token 後,會從 JWT payload 的 sub 取得使用者 id,再放進:
req.userId
後面的 controller 或 service 就可以透過 req.userId 取得目前登入者的身分,並查詢屬於這位會員的資料。
這個觀念也讓我開始理解,Token 並不是專門拿來讓畫面顯示姓名或 Email。
它更重要的角色,是讓後端在後續 Request 中確認登入身分。
我原本把 Dashboard 想得太簡單。
當時我會覺得:
先知道目前使用者是誰
→ 再取得 Dashboard 的所有資料
但實際上,PawPal 裡不同資料的來源並不完全一樣。
例如 Dashboard 顯示會員姓名時,可以直接讀:
authStore.user.name
因為登入時 user 已經被保存下來了。
但像寵物資料這類「屬於目前會員的資料」,就不是先呼叫 Current User API,再用那份結果去取得寵物。
而是:
前端帶著 Bearer Token 呼叫寵物 API
→ authenticateToken 驗證 Token
→ 從 payload.sub 取得使用者 id
→ middleware 將 id 放進 req.userId
→ 後端依 req.userId 查詢這位會員的寵物
→ 回傳結果
所以我後來才發現:
「取得目前會員資料」和「取得屬於目前會員的資源」其實也不是完全相同的流程。
前者關心的是會員本人資料。
後者則是利用已驗證的登入身分,去限制這次 Request 能取得哪些資料。
當時處理個人資料功能時,我最先注意的還是畫面。
我希望先把個人資料介面做出來,再確認 API 有沒有成功回傳資料,最後把登入者的姓名和 Email 顯示在畫面上。
流程看起來很直覺:
先完成畫面
→ 呼叫 API
→ 取得資料
→ 綁定到畫面
但實際做下去後,我才發現:
畫面做出來,不代表資料就會自動出現。
還需要確認:
只要其中一層沒有對上,畫面就可能沒有出現預期內容。
當個人資料沒有正常顯示時,我第一個懷疑的就是 API。
後來我開始一步一步檢查。
我先確認前端有沒有發出 Request,再去看後端 controller 實際回傳的 Response。
我記得當時讓我印象很深的是,問題和 Response 的資料層級有關。
後端其實有回傳資料,但前端實際讀取的位置,沒有和 Response 結構完全對上。
這段是我對當時 Debug 過程的記憶,目前已經找不到 Repository 可以完整還原那次錯誤的證據,所以我把它當成自己的開發經驗來保留。
但這次讓我學到的觀念本身沒有變:
API 成功回應,不代表前端一定取到自己以為的那一層資料。
例如以下只是簡化概念,不是 PawPal 當時的實際 Response:
{
"user": {
"name": "R-HAO",
"email": "example@example.com"
}
}
如果前端直接讀:
response.data.name
就拿不到 name。
因為 name 實際上是在:
response.data.user.name
這次我才真正感受到:
前後端不只要確認欄位名稱,連資料被包在哪一層也要看清楚。
以前遇到前端沒有資料時,我可能只會一直看畫面:
但這次我開始往後端看。
我去對照 controller 實際回傳的 Response,確認後端到底把資料包成什麼格式。
對現在來說,這可能只是很基本的 Debug 動作。
但對當時的我來說,是一個很重要的改變。
因為我不再只問:
API 有沒有成功?
而是開始問:
API 成功後,到底回傳了什麼?
看到 200 或其他 2xx 成功狀態,只能先知道這次 Request 在 HTTP 層級得到了成功的回應。
前端還是要繼續確認:
前面處理寵物資料時,我已經知道前後端欄位名稱要對得上。
這次我又多理解了一件事:
只看欄位名稱還不夠,還要看資料一路經過哪些地方。
以 PawPal 會員資料來說,至少會遇到兩種不同情境。
登入時:
Login API
→ 回傳 token + user
→ authStore 保存
→ UI 讀取 authStore.user
需要重新向後端取得目前會員資料時:
Bearer Token
→ GET /api/v1/users/me
→ authenticateToken
→ req.userId
→ 查詢會員資料
→ 回傳 { user }
而其他會員專屬資料,例如寵物資料,又會走:
Bearer Token
→ 受保護 API
→ authenticateToken
→ req.userId
→ 查詢屬於這位會員的資料
這也是我後來才真正看懂的地方。
「我是誰」這件事,在前端畫面、會員資料 API,以及其他受保護資源裡,其實會以不同方式出現。
整理到這裡後,我才比較能分清楚這兩支 API 的角色。
Login API 是登入入口,負責確認帳號密碼,成功後回傳:
token + user
Current User API 則是在已經有 Token 的情況下,讓後端依照登入身分查出目前會員資料。
所以兩者最大的差別,不是「哪一支比較重要」,而是它們處理的是會員流程中的不同階段。
一支負責登入。
另一支負責在登入後,依照身分取得目前會員資料。
這也是我以前很容易混在一起的地方。
如果現在重新遇到畫面沒有資料的問題,我不會只看到 API 成功,就直接假設資料沒有問題。
我會依序確認:
Request 是否真的送出
→ Authorization / Token 是否正確
→ HTTP 狀態是否符合預期
→ Response 完整結構是什麼
→ API 模組最後回傳哪一層
→ auth store 或其他 state 保存什麼
→ 元件實際從哪裡讀取
如果是受保護 API,我也會一起往後端確認:
authenticateToken 是否成功
→ req.userId 是否正確
→ Controller / Service 實際查了哪些資料
這樣就不會只停在:
API 有回
200,所以應該沒問題吧?
而是沿著完整資料流,一層一層確認。
這次可以先記住幾件事:
GET /api/v1/users/me 可以依照登入身分取得目前會員資料。這次最讓我有感的,不是哪一支 API 要怎麼寫。
而是我開始知道:
系統裡的「我是誰」,不是只有一個地方在處理。
前端需要知道現在畫面要顯示誰。
後端需要知道這次 Request 是誰送的。
而不同的受保護功能,又需要依照這個登入身分限制可以取得的資料。
也是從這次開始,我不再只看畫面有沒有資料,而是開始沿著:
前端
→ API
→ middleware
→ controller / service
→ Database
一路去找答案。
知道 PawPal 怎麼保存會員資料,以及後端怎麼透過 Token 辨認登入者之後,下一個問題就是:
Token 到底是什麼?後端又是怎麼驗證它的?
當時我原本以為自己已經懂 JWT。
直到面試真的被問到,我才發現很多地方其實只是「用過」,還沒有真正理解。
下一篇會接著整理 JWT 的結構、Bearer Token、middleware,以及後端如何驗證登入身分。
下一篇:
Day 13|面試被問到 JWT,我才發現自己沒有真的懂