雲端 Mac 上的 iOS Cookie 隔離驗收:工作階段邊界與登出清理

雲端 Mac 上的 iOS Cookie 隔離驗收:工作階段邊界與登出清理

雲端 Mac 上的 iOS Cookie 隔離驗收:工作階段邊界與登出清理

在同一台雲端 Mac 上執行帳號切換回歸測試,登入帳號乙後卻收到帳號甲的資料,先別急著清空模擬器。請分別檢查 Cookie 儲存區、請求標頭,以及仍在執行的任務:舊工作階段可能來自共用儲存區,也可能是網路封裝層手動寫入的標頭,甚至是登出後才抵達的回應。

以下先用 macOS 內建的網路框架進行不連網檢查,再將同一套斷言接入 iOS 模擬器。把檢查程式碼放進儲存庫,雲端 Mac 每次取出程式碼後都能重複執行,不必依賴上一次圖形介面工作階段留下的狀態。

先釐清工作階段的歸屬

為每個登入帳號建立獨立的工作階段物件,由它持有設定、Cookie 儲存區與任務集合。不要把可變的設定物件當成全域單例,也不要以為修改原始設定就能改變已建立的 URLSession:工作階段建立時會複製設定。

至少明確訂出三項約束:甲的 Cookie 不得進入乙的請求;登出時只刪除目前帳號擁有的資料;登出後抵達的舊回應不得重新啟用登入狀態。匿名請求若完全不需要 Cookie,應關閉自動處理,並確認封裝層沒有手動寫入 Cookie 請求標頭。

ephemeral 表示不將工作階段資料持久化至磁碟,不代表記憶體中沒有 Cookie,更不代表兩個應用程式帳號會自動隔離。

用腳本驗證儲存區確實獨立

將以下內容儲存為 cookie-check.swift。它只使用虛構的工作階段值,網域為本機的 localhost,不會送出網路請求,也不會存取預設的共用儲存區。

import Foundation

func require(_ condition: Bool, _ message: String) {
    if !condition {
        fatalError(message)
    }
}

func makeCookie(_ value: String) -> HTTPCookie {
    HTTPCookie(properties: [
        .domain: "localhost",
        .path: "/",
        .name: "session",
        .value: value
    ])!
}

let first = URLSessionConfiguration.ephemeral
let second = URLSessionConfiguration.ephemeral

guard let a = first.httpCookieStorage,
      let b = second.httpCookieStorage else {
    fatalError("Cookie storage unavailable")
}

require(a !== b, "Stores must be independent")

let target = URL(string: "http://localhost/account")!
a.setCookie(makeCookie("fixture-a"))
b.setCookie(makeCookie("fixture-b"))

func values(_ store: HTTPCookieStorage) -> [String] {
    (store.cookies(for: target) ?? [])
        .filter { $0.name == "session" }
        .map(\.value)
        .sorted()
}

require(values(a) == ["fixture-a"], "Account A mismatch")
require(values(b) == ["fixture-b"], "Account B mismatch")

for cookie in a.cookies ?? [] {
    a.deleteCookie(cookie)
}

require(values(a).isEmpty, "Account A was not cleared")
require(values(b) == ["fixture-b"], "Account B was affected")

在已完成命令列工具初始化的機器上執行:

set -eu
xcrun swift cookie-check.swift

正常情況下不會有任何輸出;若斷言失敗,程式會以非零結束狀態終止。這裡會清空整個 a,前提是它專屬於甲;若應用程式仍使用共用儲存區,就必須先依資料所屬帳號篩選,不能直接照搬全部刪除的做法。

這份腳本驗證的是儲存區是否獨立,以及刪除範圍是否正確,並不能證明真實請求已正確串接。不要把這一層測試通過,直接視為網路層也已通過驗收。

接入真實請求與回應

在應用程式的測試目標中重用實際的網路用戶端,分別建立甲、乙的工作階段。讓可控的測試服務透過 Set-Cookie 回傳名稱相同、值不同的 Cookie,再呼叫回顯端點,斷言伺服器收到的 Cookie 各自屬於對應帳號。

請求必須經過應用程式的封裝層,不能為了測試而暫時繞過攔截器。重點檢查預設請求標頭、重試機制與重新導向處理,是否又讀取了共用儲存區。驗收自動 Cookie 行為時,也不要手動填入請求標頭,否則可能掩蓋底層串接錯誤。

補上 Path、Secure 與到期時間的測試案例:路徑範圍較窄的 Cookie 不應出現在無關路徑的請求中;過期 Cookie 不應繼續傳送;安全 Cookie 應在受控的 HTTPS 環境中驗證。本機 HTTP 測試夾具無法證明安全連線的語意是否正確。

同名 Cookie 可以因網域與路徑不同而同時存在。斷言應比較由名稱、網域、路徑和值組成的集合,而不是假設儲存區裡永遠只有一個 session。也不要對請求標頭字串的排序做過多假設,應先解析,再比較語意。

登出時必須處理延遲回應

先將帳號狀態切換為「正在登出」,拒絕建立新任務,再取消舊任務或等待它們全部結束。接著刪除該帳號的 Cookie,讓舊工作階段失效,並為下一次登入建立新的物件。

只執行刪除會留下競態條件:舊請求稍後收到 Set-Cookie,仍可能寫回舊儲存區。取消任務也不等於同步屏障,因為回呼可能已經排入佇列。因此,每次登入都應分配工作階段世代;處理回應前先核對世代,舊世代不得更新目前的使用者狀態。

測試時先延遲甲的回應,觸發登出並登入乙,再放行甲的回應。最後同時檢查乙的介面狀態、應用程式內的帳號身分,以及下一次請求的 Cookie。只檢查登出當下儲存區是否為空,抓不到這類問題。

將驗收拆成兩層執行

每次合併程式碼時,先執行不連網的儲存區檢查腳本,再於固定的模擬器上執行請求回顯與延遲回應測試。每個測試都建立自己的設定與測試夾具,結束後停止服務、取消任務,不共用可變的工作階段,避免平行執行時互相干擾。

失敗紀錄應保留請求編號、工作階段世代、Cookie 名稱與屬性,但不要儲存真實值。需要比較值時,使用測試夾具中的固定標記,不要把正式環境的登入狀態複製到雲端 Mac。

最後,刻意讓兩個帳號共用儲存區,確認測試會失敗;再刻意移除世代檢查,確認測試能抓到延遲回應造成的問題。只有穩定偵測出這兩類回歸,才能證明這道驗收關卡檢查的是帳號邊界,而不只是幾條碰巧通過的斷言。

常見問題

使用 ephemeral 設定就不會混用 Cookie 嗎?

不能只看設定名稱。暫時性工作階段仍能在記憶體中儲存 Cookie;應分別建立設定,確認儲存物件獨立,並檢查網路封裝是否注入共用儲存區或 Cookie 標頭。

登出時呼叫 reset 就夠了嗎?

不夠。先阻擋新請求,取消或等待舊請求結束,再清除該帳號的 Cookie 並停用舊工作階段。reset 同時影響快取與憑證,不能取代明確的登出流程。

讓下一次建置擁有專屬 Mac。

選擇非虛擬機的雲端 Mac,沿用現有的 Xcode 與 CI 工作流程。依照專案負載選擇主機規格,實際可用狀態請以控制台即時回報為準。

立即租用雲端 Mac

把下一次建置交給專屬實體主機。

沿用現有指令與 CI 流程,依照專案負載選擇雲端 Mac。