昨天聊到 TDD,測試讓我開始不只在意「功能有沒有跑起來」,也更在意:它到底是不是照我想要的方式在運作。
今天就來講一個我對這件事最有感的功能——
Google 登入。
Google 登入是我在 BuJo 裡第一個做的全端功能,而它前前後後居然換了三種做法!
一開始我以為這件事很單純:
使用者按下 Google 登入,Google 幫忙確認他是誰,BuJo 把資料拿回來,登入完成。
後來它一路從 Access Token、One Tap,最後又改成 Backend OAuth Redirect,我也以為只是:
同一個功能,換了三種作法。
但真的把每一次改動拆開研究,我才發現根本不是這麼回事。
有的問題在問:
Google 怎麼證明你是誰?
瀏覽器在登入裡扮演什麼角色?
Google 已經認出你之後,BuJo 又怎麼繼續記得你?
看起來只是一顆「使用 Google 登入」的按鈕,背後其實疊了好幾層不同的概念。
所以接下來三天,我想從這個自己真的改過三次的功能開始,一層一層把登入拆開來看。
第一天,就先從我最早搞不清楚的一把「鑰匙」開始。
第一次研究 Google Login 時,我對整條流程的理解其實很模糊。
大概只知道:
BuJo 跟 Google 之間,需要有一個像鑰匙一樣的東西做認證,才能去 Google 那裡把使用者資料拿回來。
當時第一版流程大概是這樣:
BuJo 自己的 Google 登入按鈕
↓
googleTokenLogin()
↓
Google OAuth 流程
↓
Frontend 拿到 Google Access Token
↓
把 Access Token 傳給 BuJo Backend
↓
Backend 拿同一顆 Access Token 去問 Google UserInfo
↓
Google 回傳使用者資料
↓
BuJo 找到或建立 User
↓
BuJo 發自己的 JWT
↓
Frontend 存進 localStorage
這一版是真的可以登入。
Backend 會拿 Frontend 傳來的 Google Access Token,用 Bearer Token 的方式呼叫 Google UserInfo,再根據回來的 sub、email 等資料建立或找到 BuJo 使用者,最後才由 BuJo 自己簽一顆 JWT。
第一次整條流程真的跑起來時,我只覺得很神奇。
明明只是按一顆按鈕,中間卻穿過 Google、Frontend、Backend、Database,最後又回到自己的網站。
但那時候如果真的問我:
Access Token 到底是什麼?
我其實還是答不太出來。
腦袋裡一直有一個問題:
「所以它是一把鑰匙嗎?」
Google 給 Frontend 的鑰匙,和 Frontend 再送去 Backend 的鑰匙,是同一把嗎?
第一版裡,答案其實是:對,就是同一顆 Access Token。
Google 發給 Frontend,Frontend 再把它交給 BuJo Backend,Backend 最後拿它去存取 Google 的 UserInfo API。
所以「拿著一把鑰匙一路往下開門」這個想像,並不是完全錯。
真正少掉的是另一個問題:
這把鑰匙,原本到底是拿來開哪一扇門的?
我真正開始重新研究登入流程,不是因為 Google Login 壞掉,而是因為當時 BuJo 會把登入後拿到的 Token 存進 localStorage。
我一直在想:
「localStorage 是不是一個 JavaScript 可以直接讀得到的地方?那 Token 放在這裡真的安全嗎?」
原本只是想查 Token 到底應該放在哪裡,結果一路往回追,問題越長越多:
然後我第一次真的搞清楚:
Access Token 跟 ID Token 根本不是同一種用途。
我的第一個反應其實是:
「蛤?那我之前用錯了?」
更奇怪的是:
「那為什麼之前還可以登入?」
這也是我第一次很明顯地感覺到:
程式可以正常運作,和我選的是不是最適合的做法,是兩件完全不同的事。
要理解 Access Token 和 ID Token,得先把兩個常常黏在一起出現的字拆開。
Authentication,就是身分驗證。
它處理的是:
「你真的是你說的那個人嗎?」
例如 Google 告訴 BuJo:
這個 Google 帳號對應的是某個特定使用者。
這是在確認身分。
Authorization,就是授權。
它問的是:
「你被允許做什麼?」
例如 BuJo 想取得:
這些就是權限問題。
所以可以先很粗略地記:
Authentication
= 身分驗證
= 你是誰?
Authorization
= 授權
= 你可以做什麼?
而這個差異,剛好也是我一開始會把 Access Token 和 ID Token 混在一起的原因。
這裡又是一組我以前會直接統稱成「Google OAuth 登入」的東西。
OAuth 2.0 本質上是一套授權框架(Authorization Framework),用來規範:
使用者怎麼授權某個服務,去存取另一個服務裡的資源。
它最核心的問題不是:
這個人是誰?
而是:
這個 App 被允許存取哪些資源?
OpenID Connect,簡稱 OIDC。
它建立在 OAuth 2.0 之上,再加入身分驗證的規範,用來回答:
「登入的這個人到底是誰?」
而在 OIDC 裡,最核心的身分憑證之一,就是後面會看到的 ID Token。
所以如果把兩個拆開:
OAuth 2.0
→ 授權(Authorization)
OIDC
→ 身分驗證(Authentication)
明明平常都只說一句「Google Login」,底下其實同時踩著授權和身分驗證兩層。
我以前對 Token 的理解很接近:
Token = 鑰匙。
後來才發現,這個比喻不是完全錯,只是鑰匙也分很多種。
在 OAuth / OIDC 流程裡,常常會看到:
Authorization Code
Access Token
ID Token
Refresh Token
看起來全部都是一串讓人眼花撩亂的東西,但用途不一樣。
它比較像一張短暫的兌換券。
它不是拿來直接存取資料,而是用來到 Token Endpoint 交換 Token。
它比較像:
API 的通行證。
例如:
拿著 Access Token
↓
去 Google API
↓
取得這顆 Token 被允許存取的資料
它比較像:
Google 簽名的身分證明。
裡面會帶著足以讓 Backend 驗證身分的資訊,例如:
這個使用者是誰
這顆 Token 是誰發的
是發給哪一個 App
什麼時候過期
它的重點不是 「你可以存取什麼」 ,而是 「這個使用者是誰」 。
當 Access Token 到期時,可以在適合的流程裡拿它去取得新的 Access Token,而不用每次重新要求使用者授權。
這正是我當時最困惑的地方。
因為第一版不是直接把 Access Token 當成「身分證」。
它實際上走的是這條路:
Access Token
↓
Backend 拿去呼叫 Google UserInfo
↓
Google UserInfo 回傳使用者資料
↓
BuJo 用這些資料辨識使用者
所以它確實可以正常完成登入。
只是如果我的目的本來就是:
確認現在登入的這個人是誰
那 OIDC 裡的 ID Token 會是更直接的身分憑證。
因為它本來就是拿來表達使用者身分的,Backend 可以直接驗證這顆 Token 是誰發的、是不是發給 BuJo,以及有沒有過期,不需要再先拿 Access Token 去查一次 UserInfo。
這也是後來 BuJo 第二版改成:
googleOneTap()
↓
Google ID Token
↓
BuJo Backend
↓
verifyIdToken()
而不是再走原本 Access Token → UserInfo 的路線。
ID Token 裡有一組資訊欄位,通常稱為 Claims(聲明資訊)。
不用一次把全部背起來,先看幾個最常碰到的:
sub
aud
iss
exp
sub:這個使用者是誰sub 是使用者的唯一識別碼(Subject Identifier)。
可以把它想成身分提供者給這個使用者的一個固定識別。
iss:這顆 Token 是誰發的iss 代表發行者(Issuer)。
Backend 可以藉此確認這顆 Token 是不是由預期的身分提供者發出。
exp:這顆 Token 還有效嗎exp 代表到期時間(Expiration Time)。
過期的身分憑證不能一直被接受。
aud:這顆 Token 是發給誰的aud 代表這顆 Token 預期要給誰使用(Audience)。
Backend 驗證 ID Token 時,不只要知道:
這顆 Token 真的是 Google 發的
還要確認:
這顆 Token 真的是發給 BuJo 這個 Client 的
也就是說,「是真的」還不夠。
還要確認它跟我的 App 有正確的關係。
這一層是我第一版完全沒有想到的。
回頭看第一版流程,其實 Google 驗證完使用者之後,BuJo 還會再發一顆自己的 JWT。
這時候就很容易混亂:
Google 的 ID Token 跟 BuJo 的 JWT,到底是不是同一種東西?
不是。
它們都是 JWT 格式的 Token,但發行者和用途完全不同:
Google ID Token
→ Google 發
→ 證明 Google 身分
BuJo JWT
→ BuJo 發
→ 讓 BuJo 後續辨識自己的使用者
所以 JWT 比較像是一種 Token 可以採用的資料格式,不是一種登入方式。
這一層先記住就好,Day18 再接著看:
BuJo 發完自己的 JWT 之後,到底把它放去哪裡?
這次還有一個對我很有影響的地方。
當時我其實不是完全沒有查資料。
我有很認真地問 AI:
這樣做對嗎?
這樣符合業界規範嗎?
這個資訊正確嗎?
問題是,我的查證方式比較像:
找到一條路
↓
確認這條路能不能走
↓
可以
↓
繼續往前
現在回頭看,我少問的是:
這裡總共有哪幾條路?
↓
每一條的用途和限制是什麼?
↓
安全性差在哪?
↓
各自適合什麼情境?
↓
我再選
也就是從:
線性查證
變成:
發散式比較。
第一版用 Access Token 不是「功能一定不會成功」。
真正讓我改變想法的是:
「這條路走得通」跟「這是不是最符合目前需求的做法」,其實是兩件不同的事。
中間還少了一步:
把其他可行方案攤開來比較,再理解每種做法的用途、限制和取捨。
所以現在如果再跟 AI 討論同類型的技術問題,我不會只說:
「請教我一個符合業界規範的方法。」
我會再多問一層:
「還有哪些常見做法?請把用途、限制、安全性和適用情境攤開,再讓我判斷。」
對我來說,這才是第一次真正做完整功能之後,慢慢長出來的工程思考。
改成 ID Token 的時候,我真的有一種:
「好,這次應該就是正確的了!不會再有問題了吧?」
結果下一個 Bug 更奇怪。
按下 Google Login。
什麼都沒發生。
更麻煩的是——
它連一個明顯的 Error 都沒有。
明天就來看,當身分驗證改成 ID Token 之後,問題卻又跑到 Browser 身上時,這個 Google Login 又發生了什麼。