同一台云端 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 行为时也不要手填请求头,否则可能把底层接线错误遮住。
加入属性边界
补上 Path、Secure 和过期时间用例:窄路径 Cookie 不应出现在无关路径;过期 Cookie 不应继续发送;安全 Cookie 应在受控 HTTPS 环境验证。本机 HTTP 夹具不能证明安全连接语义。
同名 Cookie 可以因域和路径不同而同时存在。断言应比较名称、域、路径和值组成的集合,而不是假定存储里永远只有一个 session。也不要对请求头字符串排序做过度假设,应解析后比较语义。
退出必须处理迟到响应
先把账户状态切为“正在退出”,拒绝创建新任务,再取消或排空旧任务。随后删除该账户的 Cookie,作废旧会话,并为下一次登录建立新对象。
只执行删除存在竞态:旧请求稍后收到 Set-Cookie,仍可能写回旧存储。取消也不是同步屏障,回调可能已经排队。因此每次登录应分配会话代次;响应处理前核对代次,旧代次不得更新当前用户状态。
测试中让甲的响应延迟,触发退出后登录乙,再释放甲的响应。最终同时检查乙的界面状态、业务身份和下一次请求的 Cookie。仅检查退出瞬间存储为空,抓不到这类问题。
把验收拆成两层运行
每次合并先跑无网络存储脚本,再在固定模拟器上跑请求回显与迟到响应测试。每个测试创建自己的配置与夹具,结束后停止服务、取消任务,不共享可变会话,避免并行执行互相污染。
失败记录保留请求编号、会话代次、Cookie 名称与属性,但不要保存真实值。需要比较值时使用测试夹具中的固定标记,不要把生产登录态复制到云端 Mac。
最后故意让两个账户共用存储,确认测试会失败;再故意去掉代次检查,确认迟到响应能被捕获。能够稳定发现这两类回归,才说明门禁验的是账户边界,而不只是几条恰好通过的断言。
常见问题
使用 ephemeral 配置就不会串用 Cookie 吗?
不能只凭配置名称下结论。临时会话仍可在内存中保存 Cookie;应分别创建配置,确认存储对象独立,并检查网络封装是否重新注入了共享存储或 Cookie 请求头。
退出时调用 reset 就足够了吗?
不够。应先阻止新请求,取消或排空旧请求,再清理该账户的 Cookie,并作废旧会话。reset 还会影响缓存和凭据,不适合替代明确的账户退出流程。