Cookie、Session 与设备管理
服务端 Session 让浏览器只持有一个随机会话凭据,账号、认证强度和撤销状态留在服务端。它适合自有网站,因为“当前设备退出”“撤销其他设备”“改密后重新登录”都能映射成服务端状态变化。
Cookie 与服务端会话的分工
浏览器接收 Set-Cookie 后,按域、路径、安全属性和上下文决定是否发送 Cookie。服务端收到 Cookie 再查询 Session;浏览器里存在一个 Cookie,并不说明对应会话仍有效。
下图把首次建立会话与后续请求分开。只有全部认证因素完成后才签发正式会话,后续请求仍检查服务端状态。
存储不可用时进入受控故障路径,不按“会话有效”处理;缓存带来的状态延迟见后文。
下面是 HTTPS 站点的响应示意。<random-session-token> 需要替换为密码学安全随机数;示例的 8 小时只是 Cookie 保存时长,不代表推荐的通用会话策略。
HTTP/1.1 200 OK
Set-Cookie: __Host-session=<random-session-token>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800
Cache-Control: no-store
Content-Type: application/json
{"authenticated":true}根据 MDN Set-Cookie:
| 属性 | 作用与边界 |
|---|---|
Secure | 限制 Cookie 经安全连接发送,不替代全站 HTTPS |
HttpOnly | 阻止脚本直接读取 Cookie;XSS 仍可能代用户发请求 |
SameSite=Lax | 限制部分跨站携带行为,仍需独立 CSRF 防护 |
Path=/ | 控制发送路径,不能用作应用间安全隔离 |
不设置 Domain | 将 Cookie 限于当前主机 |
__Host- 前缀 | 支持该前缀的浏览器要求 Secure、Path=/ 且无 Domain |
Max-Age | 浏览器保存上限,服务端必须独立检查失效条件 |
跨源与跨站是不同概念。app.example.com 与 api.example.com 跨源,但在相同 scheme 等条件下可能同站;SameSite 无法替代对不可信同站子域的防护。
会话凭据与记录
会话凭据应不可预测,不编码邮箱、IP 或用户 ID。可使用 32 字节安全随机数,编码后放进 Cookie;这是设计选择。数据库保存其 SHA-256 摘要,以降低只读会话库泄露直接产生可用会话的风险。该处理适用于高熵随机凭据,不适用于低熵密码。
设备列表使用独立的非秘密 session_id,避免把 Cookie 中的凭据作为列表 ID 或日志字段。
| 字段 | 含义 |
|---|---|
session_id、token_digest | 管理标识与凭据摘要 |
user_id、security_version | 账号及创建时的安全版本 |
created_at、last_seen_at | 创建与最近活动时间 |
idle_expires_at、absolute_expires_at | 空闲与绝对过期边界 |
reauthenticated_at、auth_methods | 近期认证与认证方式 |
revoked_at、revoke_reason | 撤销状态与原因 |
| 设备描述、网络摘要 | 给用户识别设备,不能单独证明设备可信 |
不要把 IP 或 User-Agent 固定匹配作为唯一有效性条件。移动网络和浏览器升级都会改变它们,攻击者也可能伪造;更适合用作风险信号。
登录、轮换与过期
登录前的匿名会话与登录后的受信会话要轮换,防止会话固定攻击。权限提升、重要身份变化后也应重新评估是否轮换凭据。OWASP Session Management给出了会话标识更新和服务端超时要求。
每次请求至少检查会话存在、未撤销、未超时、账号可用、安全版本匹配。滑动续期只能推进空闲期限,不能越过绝对期限;敏感操作还要检查近期认证。
记录活动时间不必每个请求都写库。可以按时间粒度合并写入,但须约定空闲时间的误差上限;缓存续期与数据库持久状态也要遵守相同的绝对上限。否则持续活跃的会话可能永不结束。
凭据轮换要考虑并发请求:旧凭据立即删除会使尚在途的请求失败,长时间双有效则扩大泄露窗口。可以选择接受短暂失败、限制性过渡或按会话版本串行更新,但要验证旧响应晚到时不能把浏览器覆盖回已失效凭据。
多设备退出
“设备”通常只是产品展示单位,一个浏览器的两个 Profile 可能对应两个 Session;同一台机器也可能有多个账号会话。
| 操作 | 服务端动作 |
|---|---|
| 退出当前会话 | 撤销当前记录并清除 Cookie |
| 退出指定设备 | 验证目标会话属于当前用户后撤销 |
| 退出其他设备 | 撤销同用户除当前实例外的记录 |
| 退出全部设备 | 更新安全版本或撤销所有会话,并清除当前 Cookie |
| 修改密码或封禁 | 根据产品策略处理当前会话,同时撤销旧认证状态 |
退出使用带 CSRF 防护的状态变更请求,避免 GET 链接产生退出副作用。清 Cookie 只是清理客户端持有物,不能替代服务端撤销。删除 Cookie 时使用相同名称、路径及域范围;__Host- Cookie 不设置 Domain。
“撤销成功”通常意味着后续通过认证检查的请求被拒绝,不能自动回滚此前已进入业务事务的操作。转账等敏感写入应在提交前检查必要状态,或使用业务侧的版本约束。
缓存一致性与故障
如果服务 A 缓存 Session 30 秒,而服务 B 立即删除中心记录,A 仍可能接受旧会话。设计中应明确撤销生效的最坏时限:缓存剩余时间、事件传播和在途请求都可能贡献延迟。
按业务风险可以选择每次查询权威状态、短期缓存加可靠失效通知,或只在敏感写入重新查询。依赖不可用时,受保护操作不应降级成匿名放行;可返回明确的临时服务错误。是否允许已缓存状态继续支持低风险只读操作,需要独立的风险与时限策略。
若从旧备份恢复会话库,旧会话可能重新出现。恢复方案应验证撤销版本、全局失效代次与密钥状态,不能仅以“Redis 已恢复”作为完成条件。
跨域部署与登录回调
跨源 fetch 发送 Cookie 需要正确的凭据模式和服务端 CORS 配置;允许携带凭据时不能把允许来源设置成任意通配符,详见 MDN CORS。CORS 控制浏览器能否向脚本暴露响应,不替代认证或 CSRF 防护。
OIDC 顶层 GET 回调通常更容易与 SameSite=Lax 配合;form_post 等跨站 POST 回调需要单独验证关联 Cookie 的发送行为。不要为修复回调丢状态而全站放宽 Cookie,或在丢失状态时跳过校验。