雲端 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 同時影響快取與憑證,不能取代明確的登出流程。