iOS iframe 串接 Google 登入出現空白頁:Popup 與頁面回收問題排查紀錄
記錄 iframe 內串接 Google 第三方登入時,部分 iOS 裝置因背景頁面遭到回收並重新載入,導致 Popup 登入流程中斷,以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。
W
這次遇到的問題,發生在一個透過 iframe 嵌入的 Web 應用中。
使用者會先從宿主頁面開啟 iframe,再從 iframe 內點擊 Google 第三方登入。
大部分裝置都能正常登入,但部分較新 iOS 版本的裝置在選擇 Google 帳號後,登入流程會中斷,甚至出現空白頁,最後無法正常完成登入。
麻煩的是,這個問題並不是所有 iPhone 都能穩定重現。
當時陸續排查過 Google 帳號、Cookie、ITP、CSP、API、iframe 權限等方向,最後透過前端診斷紀錄與不同裝置的對照測試,才找到真正影響流程的原因。
當時的登入架構
整體流程可以簡化成:
原本A頁面
↓
開啟 iframe
↓
使用者在 iframe 內點擊 Google 登入
↓
另外開啟 Google 登入分頁
↓
A頁面與 iframe 進入背景
↓
使用者選擇 Google 帳號
↓
Google 完成驗證後關閉分頁
↓
回到原本A頁面登入成功畫面
這個流程有一個很重要的前提:
原本等待 Google 登入結果的頁面必須還存在。
但到了部分 iOS 裝置,問題就出現在這裡。
問題出在背景頁面遭到回收
Google 登入分頁開啟之後,原本的宿主A頁面與 iframe 會退到背景。
正常情況下,iOS 該頁面應該會進行等待確認 google 是否已登入的動作:
宿主A頁面進入背景
↓
進行等待確認 google 是否已登入動作
↓
頁面仍然存在
↓
回到頁面
↓
接收到google已登入流程
↓
登入流程繼續並完成
但在發生問題的裝置上,實際觀察到的是背景頁面遭到系統回收。
等使用者完成 Google 登入再回來時:
Google 登入分頁開啟
↓
宿主A頁面進入背景
↓
iOS 回收背景頁面
↓
使用者回到原頁面
↓
頁面重新載入
↓
iframe 重新建立
↓
原本的 頁面、callback 與暫存狀態消失
因此真正造成問題的不是單純「背景頁面被暫停」。
而是:
頁面遭到回收後發生 Reload,原本等待 Google 登入結果的 JavaScript 執行環境已經不存在。
為什麼 Popup 登入會受到影響?
Popup 類型的登入流程,可以簡化成:
iframe 內的 Web 應用
↓
開啟 Google 登入分頁
↓
Google 完成驗證
↓
把登入結果交回原本的登入流程
只要原頁面還活著,流程就能繼續。
但如果 Google 登入期間原頁面遭到 iOS 回收:
Google 完成登入
↓
準備回傳登入結果
↓
原頁面已經 Reload
↓
原本的 callback 不存在
↓
Popup 登入流程無法接續導致一片空白
↓
手動切回A頁面反而重整登入失敗
這也讓問題一開始很難從 API 或 Server Log 找到。
因為真正出問題的時候,不一定是 Google 回傳了一個錯誤,而可能是:
原本應該接收登入結果的頁面已經被重新載入。
最後的處理方式:依 iOS 版本分流
最後並沒有把所有裝置全部改成 Redirect,而是依裝置與 iOS 版本進行分流。
概念大致如下:
Google Login
↓
判斷裝置與 iOS 版本 (IOS26以上走 Redirect, IOS26以下走舊有流程)
↓
較新的 iOS
└─ Redirect 登入
其他裝置 / 舊版 iOS
└─ 維持原本 Popup 登入
原本能正常使用 Popup 的環境繼續維持既有機制,而容易受到背景頁面回收影響的新版 iOS,則改走 Redirect。
Redirect 後還要能接回登入流程
單純改成 Redirect 還不夠。
因為頁面仍然可能在登入期間被 iOS 回收,所以在離開頁面之前,需要先保存一個能辨識「這一次登入流程」的資訊,例如:
loginKey
state
transactionId
流程就變成:
iframe 內開始 Google 登入
↓
先保存 loginKey / state
↓
前往 Google Login
↓
Google 完成驗證
↓
Redirect 回系統
↓
原頁面如果已被回收就重新載入
↓
讀取之前保存的 loginKey
↓
向 Server 確認登入結果
↓
重新接回登入狀態
最大的差別在於:
頁面 Reload 不再等於登入流程直接失敗。
就算原本的 JavaScript 資料已經不存在,只要還保留這一次登入流程的識別資訊,就有機會重新向 Server 接回登入結果。
不要直接把 Google Token 存進 localStorage
這裡的「保存登入資訊」並不是指把 Google Access Token 或 ID Token 長時間放進 localStorage。
比較適合保存的是短效、不能直接代表登入身分的識別值,例如:
localStorage.setItem("googleLoginState", loginKey);
頁面重新載入後,再由這個識別值向 Server 查詢這次登入是否完成。
這樣保存的是「我要接回哪一次登入流程」,而不是直接把真正的登入憑證留在瀏覽器中。
降低記憶體只能治標
另外一個可以做的方向,是在進行第三方登入前暫停一些比較耗資源的內容,例如:
- WebGL / Canvas
- 大型動畫
- 影片
- 高頻 Timer
- 不必要的背景工作
這些作法有機會降低背景頁面的記憶體壓力。
但它只能降低頁面遭到回收的機率,不能保證 iOS 一定不會回收背景頁面。
因此與其把設計建立在:
頁面一定不能被 Reload。
更可靠的方向應該是:
就算頁面被 Reload,重要流程仍然能夠恢復。
這次踩坑後的心得
桌面瀏覽器上很常見的第三方登入流程:
開 Popup
→ 等 callback
→ 取得登入結果
→ 完成登入
到了行動裝置,不一定能假設原本的 JavaScript Context 會從流程開始一路存在到結束。
尤其當流程包含:
- iframe
- 第三方 OAuth
- 開啟新分頁
- 頁面進入背景
- Redirect
重要流程最好具備可以重新接回的機制。
這次最後真正解決的不只是「Google 登入空白頁」這個問題。
更重要的是重新認識:
不要假設頁面不會消失,而是讓流程在頁面被重新載入後,仍然有辦法接著走下去。