会话不仅是 Cookie 指向的服务器状态,而是跨越 API、后台 Worker、WebSocket 和 AI Agent 的实时信任决策,扩展含义才是真正难点。
会话不仅仅是一个指向服务端状态的 cookie。它是一个活的信任决策,已经逃逸出登录服务,开始在你的架构中流动。它涉及 API、后端 Worker、WebSocket 连接、移动设备,以及如今那些可以在用户关闭浏览器后继续工作的 AI Agent。扩展查找是容易的部分,扩展这种信任的含义才是系统容易出问题的地方。
登录时情况看起来很清晰。用户证明身份,也许完成了 MFA,收到一个会话标识符。服务器存储一条包含用户、过期时间和若干声明的记录。每个请求都携带 cookie,应用程序加载该记录。
然后现实来了。用户更换了组织。管理员删除了一个角色。风险信号需要再次进行 MFA 检查。用户在一个设备上登出但没有在另一个设备上登出。账户恢复后,支持团队需要撤销所有活跃会话。原始登录是有效的,但它周围的授权上下文几乎一直在变化。
如果会话是一个带 30 天过期时间的签名 blob,且没有服务端控制权,那些变更就要等待 30 天才会生效。密码学有效性并不等同于当前授权。

这是第一个重要的设计决策:什么必须在每个请求时检查,什么可以安全地保持固定直到过期。用户身份在会话期间可能保持稳定。但租户成员资格、账户状态、风险级别和敏感权限通常不是。
浏览器应该在 cookie 中保存一个不透明、高熵的会话标识符,并配置 Secure、HttpOnly 和适当的 SameSite 策略。它不应该在 cookie 值里携带完整授权模型,让每个服务都独立信任它。
RFC 6265 定义了 cookie 行为,但 cookie 标志只是传输控制。它们帮助保护标识符,但不能解决轮换、撤销或授权新鲜度问题。
服务端会话记录可以保持精简:
type SessionRecord = {
id: string;
subjectId: string;
tenantId: string;
createdAt: number;
lastSeenAt: number;
idleExpiresAt: number;
absoluteExpiresAt: number;
authLevel: "password" | "mfa";
version: number;
revokedAt?: number;
};
注意其中没有包含:登录时用户拥有的每个权限的复制列表。将稳定的会话事实保存在记录中,然后将可能变化的授权从当前策略或成员源中解析出来。否则会话存储就变成了过期授权的仓库。
会话过期回答了凭证可以存活多长时间。轮换回答了同一凭证是否应该跨越安全边界(如登录、MFA 完成、密码重置、租户切换或权限提升)继续存活。
如果没有轮换,登录前捕获的标识符在登录后可能变成已认证的会话。这就是会话固定攻击。如果没有权限提升后的轮换,相同的长期标识符在没有任何新边界的情况下从普通访问转移到敏感访问。
安全的模式是原子性地创建一个新标识符并使旧标识符失效:
async function rotateSession(oldId: string, update: Partial<SessionRecord>) {
return sessionStore.transaction(async (tx) => {
const current = await tx.getForUpdate(oldId);
if (!current || current.revokedAt) {
throw new Error("session is no longer active");
}
const next = {
...current,
...update,
id: crypto.randomUUID(),
version: current.version + 1,
lastSeenAt: Date.now(),
};
await tx.revoke(oldId, Date.now());
await tx.insert(next);
return next;
});
}
原子性很重要。如果在竞态条件下两个标识符都保持有效,轮换就创建了第二个会话而不是替换第一个。
集中式会话存储给了你一个撤销点,但只有每条路径都实际检查它才会生效。一个普通 HTTP 请求可能在每次调用时加载会话,而 WebSocket 认证一次并保持连接数小时。队列任务可能已将 subject 和 tenant 复制到其负载中。内部服务可能将成功的会话检查缓存超过撤销目标时间。
这就是为什么一个有用的会话设计从撤销目标开始。如果禁用账户必须在五分钟内停止敏感操作,每个缓存、连接和 Worker 都必须在五分钟内重新验证或收到可靠的撤销事件。只在 Redis 里写入 revokedAt 对一个从不重新查询的进程毫无作用。

对于高风险操作,立即检查当前会话状态,当认证级别不足时要求 step-up 认证。对于较低风险的读取,短暂的缓存可能是合理的。策略应该是明确的。随意的缓存持续时间不是安全策略。
NIST SP 800-63B 在这里很有用,因为它将整体会话生命周期与无活动超时和重新认证要求分开。这些控制不应该合并成一个 TTL 字段。一个活跃会话仍然可以达到其绝对最大值,而一个在其最大值内的会话仍然可以在敏感操作前要求重新认证。
Agent 任务通常被视为用户浏览器会话的后台延续。用户启动一个任务,Agent 收到一些凭证,然后它继续运行。但浏览器会话可能在任务仍在做决策时结束。如果 Agent 只是复制了原始 bearer token,logout 就变得几乎是名义上的了。
Agent 需要自己的有界执行会话。它应该标识工作负载、保留谁委托了任务、定位一个租户,并只携带该任务所需的范围。它的生命周期应该反映工作,而不是用户浏览器 cookie 的生命周期。如果它委托给另一个 Agent 或工具,下游凭证应该变得更窄,而不是继承所有东西。
RFC 8693 的 OAuth 2.0 Token Exchange 为创建下游凭证提供了一个有用的模式。浏览器会话建立人类上下文,但 Agent 收到一个单独的短期令牌,有它自己的受众和可追溯的委托。撤销任务可以停止 Agent 而不破坏每个用户会话,当策略要求时撤销用户仍然可以级联到活跃任务。
这种区分也改善了审计日志。"用户 123 进行了 API 调用" 在自主工作负载在用户点击按钮 20 分钟后执行了调用时是不完整的。记录会话、Agent 身份、委托主题、租户和任务标识符。否则所有自主工作都变得与直接人类行为无法区分。
集中式会话存储成为关键基础设施,所以团队在它不可用时会倾向于 fail open。这将一个操作事件转化为授权绕过。如果服务无法确定敏感会话是否活跃,它不应该假定为活跃。
你可以通过副本、地域存储、仔细有界本地缓存和签名短期证明来降低可用性风险。但每个优化都需要一个最大过期信任窗口。一个 5 分钟的缓存意味着已撤销的访问可能继续持续 5 分钟。有时候这是可接受的。假装它只是一个性能设置就不是了。
监控身份结果,而不仅仅是缓存延迟。追踪被拒绝的过期会话、已撤销会话的重用、轮换失败、不可能的租户切换,以及撤销需要多长时间才能到达长期连接。一个会话系统可以有完美的正常运行时间而授权新鲜度完全崩溃。
大规模会话管理的工作是保持信任在其传播时的即时性。在浏览器中尽可能少地存储,当信任改变时轮换标识符,分隔空闲和绝对生命周期,并使撤销在已知窗口内到达每个执行路径。
对于 AI Agent,不要拉伸人类浏览器会话来覆盖自主工作。创建一个更小的、可追踪的执行会话,有它自己的身份和过期时间。会话跨越服务边界的那一刻,它就成了身份架构,就是这样。
Akash Devdhar 是一位高级软件工程师,专攻企业身份、认证、授权和 AI 基础设施。他写关于使用 OAuth、OIDC、RBAC 和现代身份架构构建安全 AI 系统的文章。