Files
XQKqueue/docs/login-persistence-design.md
2026-07-12 15:53:24 +08:00

5.1 KiB
Raw Permalink Blame History

登录持久化设计

1. 目标

为管理端和员工端提供正式上线可用的登录持久化:

  • 刷新页面、关闭后重新打开浏览器、前后端服务重启后仍可恢复登录。
  • 会话仅保存在服务端数据库,浏览器仅保存不透明随机令牌。
  • 员工端和管理端会话相互隔离,可在同一浏览器同时登录。
  • 支持闲置过期、绝对过期、主动退出、停用账号立即失效和管理员撤销会话。
  • 不在 localStoragesessionStorage 中保存密码、会话令牌或完整用户资料。

2. 现有基础

当前已实现:

  • auth_sessions 数据库表持久化会话。
  • 会话令牌使用高熅随机值,数据库只保存 SHA-256 摘要。
  • Cookie 使用 HttpOnlySecureSameSite=Strict 和明确的 Expires/Max-Age
  • 管理端与员工端使用不同 Cookie 名称。
  • 前端启动时调用 /api/{portal}/auth/me 恢复用户状态。
  • 退出时撤销数据库会话并清除 Cookie。

当前的 SESSION_TTL=12h 是固定过期时间,last_seen_at 尚未用于闲置过期或续期。

3. 会话策略

策略 管理端 员工端
闲置超时 12 小时 24 小时
绝对有效期 7 天 30 天
滑动续期 剩余闲置时间低于 6 小时时续期 剩余闲置时间低于 12 小时时续期
并发会话上限 每账号 5 个 每账号 10 个
超限策略 撤销最早活跃会话 撤销最早活跃会话

续期只能延长闲置截止时间,不得超过绝对有效期。超级管理员不豁免会话过期和撤销规则。

4. 数据模型

auth_sessions 中保留现有字段,新增:

absolute_expires_at timestamptz NOT NULL,
last_rotated_at timestamptz NOT NULL DEFAULT now(),
revoked_reason varchar(32),
device_label varchar(120) NOT NULL DEFAULT ''

索引:

  • 保留 token_hash 唯一索引。
  • 增加 (user_id, last_seen_at DESC) WHERE revoked_at IS NULL,用于会话列表和并发上限。
  • 增加 (expires_at) WHERE revoked_at IS NULL,用于过期会话清理。

revoked_reason 取值:LOGOUTACCOUNT_DISABLEDPASSWORD_CHANGEDADMIN_REVOKEDCONCURRENT_LIMITEXPIRED

5. 请求验证与续期

每个受保护请求执行:

  1. 读取当前端对应的 HttpOnly Cookie。
  2. 对令牌做 SHA-256查询未撤销会话。
  3. 同时校验 expires_atabsolute_expires_at
  4. 校验用户仍启用且角色与当前端一致。
  5. 距上次写入超过 5 分钟时才更新 last_seen_at,避免每次 API 请求写库。
  6. 进入续期窗口时,更新 expires_at 并重发 Cookie新时间不超过 absolute_expires_at
  7. 会话无效时清除 Cookie 并返回统一 401 SESSION_INVALID

第 5、6 步应使用条件更新,保证多个并发请求不会频繁写入。

6. 前端恢复流程

  • 应用启动后保持鉴权 loading 状态,先请求 /auth/me,不提前闪现登录页。
  • /auth/me 成功:只将返回的用户和项目权限保存在 React 内存状态。
  • 返回 401清空内存状态并跳转到对应登录页。
  • 其他网络错误:显示“无法确认登录状态”和重试入口,不直接判定为退出。
  • 所有 API 对 401 使用统一事件通知鉴权容器,避免页面在会话过期后继续停留在受保护界面。
  • 可持久化项目选择等非敏感偏好,但恢复后必须再与 /auth/me 返回的权限集合校验。

7. 会话管理接口

  • GET /api/{portal}/auth/sessions:列出当前账号的活跃会话,仅返回会话 ID、设备摘要、IP 脱敏值、创建时间和最后活跃时间。
  • DELETE /api/{portal}/auth/sessions/{id}:撤销指定会话。
  • POST /api/{portal}/auth/logout-all:撤销当前账号全部会话。

管理员修改普通账号密码、角色或停用账号时,必须在同一数据库事务中撤销该账号的所有会话。

8. 清理与运维

  • API 实例每小时尝试清理一次,或由部署平台定时任务执行。
  • 已过期或撤销会话保留 30 天供安全审计,然后物理删除。
  • 监控指标:登录成功/失败数、限流数、活跃会话数、续期数、无效会话数和会话查询耗时。
  • 不记录原始 Cookie、令牌、密码或完整 IP。

9. 上线迁移

  1. 新增字段允许短暂为空,对存量会话使用当前 expires_at 回填绝对过期时间。
  2. 部署兼容新旧会话的 API完成回填后再加 NOT NULL 约束。
  3. 部署前端统一 401 处理和会话状态页。
  4. 启用过期会话清理任务和监控。

迁移不需要强制所有用户重新登录。

10. 验收标准

  • 登录后刷新页面、关闭并重开浏览器、重启 API 均能恢复登录。
  • 管理端和员工端会话同时存在且互不覆盖。
  • 闲置期内活动能续期,但不能突破绝对有效期。
  • 停用账号、改密码、改角色、主动退出或管理员撤销后,旧会话立即不可用。
  • Cookie 中无用户资料,前端存储中无密码和会话令牌。
  • 网络故障不会被误判为退出,真实 401 会统一返回登录页。
  • 并发请求不会导致每次请求更新会话表。