示例代码均为匿名化改写,用于说明模式;Keychain 读写细节需按应用的安全策略实现。
凭据与界面状态分开
共享认证状态负责当前用户、令牌和会话版本;独立的存储层负责 Keychain 读写。凭据使用设备解锁后可访问、且不迁移到其他设备的保护级别。视图读取会话状态,不直接操作 Security API。
服务端判定某个令牌失效时,应先确认失败请求使用的令牌仍属于当前会话。否则一个旧请求可能在新用户登录之后返回,再错误地清除新会话。
为什么需要会话快照
检查“当前已登录”不足以判断请求归属。请求发出后,用户可能退出并登录另一账号;旧请求返回时,登录状态依然为真,但响应数据属于旧用户。
请求开始时记录会话版本和身份快照,完成后再比较。页面请求 ID 可以拦截同一页面上较晚发出的查询;会话版本则拦截跨账号的迟到结果。两项检查共同决定响应是否仍能更新界面。
struct SessionSnapshot: Equatable, Sendable {
let accountID: UUID
let generation: UInt64
}
actor SessionStore {
private var current: SessionSnapshot?
func snapshot() -> SessionSnapshot? { current }
func replaceAccount(with accountID: UUID) {
let nextGeneration = (current?.generation ?? 0) + 1
current = SessionSnapshot(
accountID: accountID,
generation: nextGeneration
)
}
func isCurrent(_ snapshot: SessionSnapshot) -> Bool {
current == snapshot
}
}
账号切换时,仅检查“仍然已登录”不足以判定迟到响应的归属;请求应同时比对账号标识和会话代次。
退出失败也要保持状态一致
如果删除安全存储失败,应用不应显示退出成功,却在下次启动时恢复旧凭据。可以恢复先前的内存会话并提示失败,或用独立失效标记阻止已判无效的令牌被恢复;具体选择要与账号状态模型一致。
SecItemDelete 成功后才能把界面切到退出状态。删除失败时保留当前内存会话并提示可恢复的错误,避免应用显示已退出、下次启动却恢复旧凭据。
实践要点
- 凭据不写入普通偏好设置。
- 异步结果同时校验页面请求身份和认证会话身份。
- 认证失败只影响仍然匹配的当前令牌。
- 账号切换时清除旧的个人资料与敏感页面状态。
