云端 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 行为时也不要手填请求头,否则可能把底层接线错误遮住。

补上 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。