iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Vibe Coding

夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略系列 第 17

Day17|明明路都走對了,為什麼還是卡關?從瀏覽器 Bug 看懂 Google 登入怎麼改走另一條路

  • 分享至 

  • xImage
  •  

昨天,我們終於把 Google Login 裡那些很容易混在一起的 Token 拆開了。

BuJo 第二版也從原本的 Access Token,改成用 ID Token 來確認登入者身分。

當時我真的有種:

「好!這次應該終於走對了吧?」

結果某次按下 Google 登入之後——

沒反應。

我打開主控台看,後端也沒有出現任何明顯的錯誤訊息。

前前後後查了好~久,最後才發現:

問題居然出在瀏覽器擴充功能!

停用之後,Google Login 就恢復正常了。

我當下真的只剩:

「蛤???這誰想得到?」

而這次真的碰到瀏覽器這一層的問題,也成了我們後來決定把 Google Login 從第二版改成第三版的原因之一。

只是第三版真正換掉的,遠不只是「登入畫面改成會跳頁」。

整條 OAuth Flow 裡,Frontend、Backend、瀏覽器和 Google 之間負責的事情,都跟著重新分配了。


第二版到底卡在哪一層?

第二版我們用的是 Google One Tap。

它就是我們平常逛網站時,會浮在目前頁面上的那種小型 Google 登入提示。

這種做法我自己其實滿喜歡的。

因為使用者不用整頁離開 BuJo,選完帳號就可以直接登入。

整個過程也不會打斷原本正在看的頁面。

使用者按下 BuJo 的 Google 登入按鈕後,會叫出 Google One Tap。

整條流程大概是:

BuJo Google 登入按鈕
↓
Google One Tap
↓
Frontend 拿到 Google ID Token
↓
Frontend 把 ID Token 傳給 BuJo Backend
↓
Backend 驗證 ID Token

但第三方登入跑在瀏覽器裡,也代表中間不是只有自己的 Frontend 和 Backend。

如果把一次登入可能經過的層次拉開來看:

UI
↓
Frontend
↓
第三方 SDK
↓
瀏覽器/執行環境(Browser / Environment) ← BuJo 這次卡在這裡
↓
Network
↓
Backend

瀏覽器這一層,本來就還可能受到很多東西影響:

瀏覽器/執行環境(Browser / Environment)
├─ 瀏覽器擴充功能(Browser Extension)
├─ 彈窗/登入提示行為(Popup / Prompt)
├─ Cookie/儲存政策(Cookie / Storage Policy)
├─ 隱私與追蹤保護(Privacy / Tracking Protection)
└─ 聯合登入機制(Federated Login)

而這次 BuJo 遇到的 Bug 真正定位到的,就是:

瀏覽器擴充功能(Browser Extension)。

這也不是我們唯一一次在瀏覽器這一層碰到狀況。

測試時,Safari 也曾經出現不能正常登入的情況,另外也遇過可能和不同來源或瀏覽器端政策有關的問題。

這些經驗讓我開始更在意:

第三方登入不是 Code 寫對、Backend 接得住就一定會正常,瀏覽器本身也會參與在這條流程裡。


V2 → V3,真正換掉的是誰在主導 OAuth Flow

找到瀏覽器擴充功能的問題之後,當下的登入確實恢復正常了。

但前面幾次測試累積下來,我開始有一個更實際的擔心:

如果之後是真實使用者遇到 Safari 不能登入,或其他瀏覽器端的狀況呢?

尤其這類問題不一定會直接跳出一個很明顯的錯誤訊息,使用者可能只會覺得:

「我按了,為什麼沒反應?」

也是因為這個考量,後來第三版,我們決定把 Google Login 改成由後端主導的 OAuth 重新導向流程

也就是 OAuth 2.0 的:

Authorization Code Flow(授權碼流程)。

如果只看使用者看到的畫面,差異其實很好認。

One Tap 是在目前頁面上浮出 Google 登入提示;第三版則會整頁前往 Google,完成登入後再回到 BuJo。

但真正重要的差別,不是「有沒有跳頁」。

而是:

Google 的登入結果在哪一層被接住,以及後面的 OAuth Flow 由誰負責。

第二版:One Tap                    第三版:Authorization Code Flow

BuJo Frontend                     BuJo Frontend
      ↓                                 ↓
googleOneTap()                    BuJo Backend
      ↓                                 ↓
Google One Tap                    建立 Google Authorization URL
      ↓                                 ↓
Frontend 拿到 ID Token            瀏覽器 → Google
      ↓                                 ↓
Frontend 傳給 Backend             Callback 回 Backend
      ↓                           (code + state)
Backend 驗證 ID Token                   ↓
                                  Backend 用 code 換 Token
                                        ↓
                                  Backend 驗證 ID Token

所以第二版是:

Frontend 先從 Google 拿到 ID Token,再交給 Backend。

第三版則變成:

Frontend 先把登入交給 Backend,後面的 Authorization Code Exchange 也由 Backend 處理。

也就是說,第三版不是單純把 One Tap 換成另一種 Google Login UI。

真正重新分配的,是整段 OAuth 流程裡,各自負責的工作。


第三版的 Authorization Code Flow,到底一路在傳什麼?

光看到:

Frontend
↓
Backend
↓
Google
↓
Backend

其實還是很難懂。

我真正開始看懂這條 Flow,是把每一條箭頭拆開問:

「現在是誰把什麼東西交給誰?」

先把完整路線攤開:

① Frontend
   ↓
   BuJo Backend
   (開始 Google Login)

② BuJo Backend
   ↓
   建立 Google Authorization URL
   (client_id、redirect_uri、response_type、scope、state)
   ↓
   瀏覽器被重新導向到 Google

③ Google
   ↓
   使用者完成登入
   ↓
   瀏覽器被導回 BuJo Backend Callback
   (code + state)

④ BuJo Backend
   ↓
   驗證 state
   ↓
   POST Google Token Endpoint
   (Request Body 帶著 code、client_id、client_secret、
    redirect_uri、grant_type)

⑤ Google
   ↓
   回傳 Token Response
   ↓
   BuJo Backend 驗證 ID Token

接下來就照這條路,一段一段打開。


第一步:Backend 建立 Google Authorization URL,是在建立什麼?

Frontend 先把 Google Login 交給 Backend。

這時 Backend 會先組出一條:

等等要讓瀏覽器前往 Google 登入頁面的網址。

這條網址就是 Google 的授權端點(Authorization Endpoint)URL,後面再帶上這次登入需要的 Query Parameters。

概念上會像:

https://accounts.google.com/o/oauth2/v2/auth
?client_id=...
&redirect_uri=...
&response_type=code
&scope=openid%20email%20profile
&state=...

也就是:

「等等要讓瀏覽器前往 Google 登入頁面的網址,並且把這次登入需要的設定一起寫在網址後面。」

後面的每一個參數,都在告訴 Google 不同的事情:

client_id
→ 我是哪一個 OAuth Client?

redirect_uri
→ 登入完成後,要把瀏覽器送回哪裡?

response_type=code
→ 這次走 Authorization Code Flow,
  登入完成後先回傳 Authorization Code

scope
→ 這次需要哪些身分/權限範圍?

state
→ 這次 OAuth Request 帶出去的比對值

所以 Backend 做的第一件事,可以先理解成:

BuJo Backend
↓
建立 Google Authorization URL
(帶著 client_id、redirect_uri、response_type、scope、state)
↓
回傳 Redirect Response
(告訴瀏覽器:請前往剛剛建立好的 Google Authorization URL)
↓
瀏覽器自動跳到 Google
↓
使用者看到 Google 的登入頁面

也就是說,Backend 不是自己跑去 Google 登入。

它做的是:

先把「要去哪裡+這次登入需要的設定」組成一條網址,再透過 Redirect 告訴瀏覽器前往那個網址。

真正被送去 Google 的,還是使用者現在正在操作的瀏覽器


第二步:Google 登入完成後,怎麼回到 BuJo?

使用者在 Google 完成登入之後,瀏覽器會被導回 BuJo 預先設定好的回呼網址(Callback URL):

瀏覽器被導回 BuJo Backend Callback
(帶著 code + state)

Callback URL 可以先理解成:

我們事先告訴 Google:「登入完成之後,請把這趟流程送回這個 Backend Endpoint。」

所以這一步最重要的事情其實只有兩個:

Google 登入完成
↓
瀏覽器回到 BuJo Backend
(帶著 code + state)

其中的 code,就是昨天看過的 Authorization Code。

Backend 收到之後,下一步才會拿它去 Google Token Endpoint 換 Token。


第三步:Backend 拿 Code 去 Google,到底送了什麼?

這一段跟前面的 Redirect 不一樣。

前面是:

瀏覽器
↓
被重新導向到 Google

但現在是:

BuJo Backend
↓
主動送一個 HTTP POST Request
↓
Google Token Endpoint

目的很單純:

拿剛剛收到的 Authorization Code 去換 Token。

這次要交給 Google 的資料,不是放在跳轉網址上,而是放進 HTTP POST Request 的 Body。

BuJo 送出的內容可以先這樣看:

BuJo Backend
↓
POST Google Token Endpoint

Request Body
├─ grant_type=authorization_code
│  → 告訴 Google:這次要用 Authorization Code 換 Token
│
├─ code=剛剛收到的 Authorization Code
│  → 就是 Google 透過 Callback 帶回來的那張兌換碼
│
├─ client_id=BuJo 的 Client ID
│  → 告訴 Google:這個 Request 是哪一個 OAuth Client 發出的
│
├─ client_secret=BuJo Backend 保存的 Client Secret
│  → Backend 用來向 Google 識別/驗證這個 OAuth Client 的秘密值
│
└─ redirect_uri=BuJo 的 Callback URL
   → 告訴 Google:這次 Code 原本是搭配哪一個 Callback URL 使用

這時候整段 Code Exchange 就比較不像魔法了。

Backend 其實是在跟 Google 說:

「這是你剛剛發給我的 Code。我是 BuJo,這是這次登入使用的 Callback。請確認這一整組資料對得起來,再把 Token 給我。」

Google 確認這次交換沒問題之後,才會回傳 Token Response。

接著 BuJo Backend 再驗證裡面的 ID Token。

而 ID Token 到底要驗什麼,昨天已經拆過了,今天就不重講一次。


state 為什麼要一起出去,又一起回來?

走到這裡,還剩下一個很重要的東西:

state

前面的路線是:

BuJo
↓
Google
↓
BuJo

使用者的瀏覽器被送出去一趟,又從 Google 回來。

那 BuJo Backend 怎麼知道:

「現在回來的這個 Callback,真的對應我剛剛送出去的那一次 OAuth Request 嗎?」

這就是 state 的工作。

流程可以這樣看:

BuJo Backend 發起登入
(產生 state)
↓
Google Authorization Request
(帶著 state)
↓
使用者完成 Google Login
↓
瀏覽器被導回 BuJo Callback
(帶著 code + state)
↓
BuJo Backend 驗證 state

所以 state 不是拿來證明:

「這個使用者是誰?」

它在處理的是:

「現在回來的這一趟,是不是剛剛從我這裡出去的那一趟?」

除了把送出去的 OAuth Request 和回來的 Callback 對起來之外,state 也是 OAuth 裡防護跨站請求偽造(CSRF)的重要機制。

我覺得把它放進完整 Flow 之後,比單獨背一句「OAuth 要有 state」好懂很多。

因為這時候我真的看得到:

它為什麼要出去,又為什麼一定要跟著回來。


回頭看這次 Debug,我後來才整理出一張地圖

第三版的流程拆完之後,再回頭看一開始那個「按了沒反應」的 Bug,我覺得這次還留下另一個很好用的東西。

就是:

先確認問題最後成功走到哪一層。

可以先把路切成:

UI
↓
Frontend
↓
第三方 SDK
↓
瀏覽器/執行環境(Browser / Environment) ← BuJo 這次
↓
Network
↓
Backend

如果按鈕事件根本沒有觸發,就還不用急著查 Backend。

如果 Frontend 已經往下走,卻沒有真的送出 Request,就可以先把範圍留在 Frontend、第三方 SDK 或瀏覽器這幾層。

如果 Request 已經成功進 Backend,再往後查 Authentication、資料或其他後端問題。

我當時並不是拿著這張完整地圖照表 Debug。

是經過這次真的卡很久之後,我才慢慢把它整理成現在比較會用的方式:

先定位,再找原因。

這次真正值得留下來的,也不是:

「下次 Google Login 壞掉,記得先把 Extension 關掉。」

因為下一個 Bug 根本不一定長一樣。

比較有用的是:

就算一開始不知道答案,還是可以先讓問題範圍越來越小。


原來第三版不是「又換一種登入寫法」

一開始看到 Google Login 前前後後換了三次,我真的很容易把它想成:

同一個功能,只是又換一種實作方式。

但把第二版和第三版完整拆開之後,我現在看到的已經不是:

One Tap → 整頁重新導向。

而是:

第二版
Frontend 直接接住 Google ID Token
↓
再交給 Backend 驗證

第三版
Backend 發起 Authorization Code Flow
↓
瀏覽器負責在 BuJo 和 Google 之間重新導向
↓
Callback 帶回 code + state
↓
Backend 再自己拿 Code 去 Google 換 Token

真正被重新分配的,是:

這整段 OAuth Flow 到底由誰負責。

也因為真的碰過瀏覽器擴充功能這次問題,我們才會重新思考:

是不是可以不要再讓登入入口這麼依賴 One Tap 的瀏覽器端行為?

最後才一路走到第三版。

Google 這邊終於把「你是誰」確認完了。

但故事還沒有真的結束。

因為 Google 認得你,不代表 BuJo 接下來的每一個 Request 都會自動記得你是誰。

下一篇,就來看另一把真正屬於 BuJo 自己的鑰匙:

Google 都已經驗完身分了,BuJo 為什麼還要再發自己的 JWT?


上一篇
Day16|拿到鑰匙就算會開門了嗎?從 Google Login 看懂 Access Token 與 ID Token
下一篇
Day18|Google 都放你進城了,BuJo 為什麼還要再發一張通行證?從 JWT、Cookie 到 CORS
系列文
夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言