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

114 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 登录持久化设计
## 1. 目标
为管理端和员工端提供正式上线可用的登录持久化:
- 刷新页面、关闭后重新打开浏览器、前后端服务重启后仍可恢复登录。
- 会话仅保存在服务端数据库,浏览器仅保存不透明随机令牌。
- 员工端和管理端会话相互隔离,可在同一浏览器同时登录。
- 支持闲置过期、绝对过期、主动退出、停用账号立即失效和管理员撤销会话。
- 不在 `localStorage``sessionStorage` 中保存密码、会话令牌或完整用户资料。
## 2. 现有基础
当前已实现:
- `auth_sessions` 数据库表持久化会话。
- 会话令牌使用高熅随机值,数据库只保存 SHA-256 摘要。
- Cookie 使用 `HttpOnly``Secure``SameSite=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` 中保留现有字段,新增:
```sql
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` 取值:`LOGOUT``ACCOUNT_DISABLED``PASSWORD_CHANGED``ADMIN_REVOKED``CONCURRENT_LIMIT``EXPIRED`
## 5. 请求验证与续期
每个受保护请求执行:
1. 读取当前端对应的 HttpOnly Cookie。
2. 对令牌做 SHA-256查询未撤销会话。
3. 同时校验 `expires_at``absolute_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 会统一返回登录页。
- 并发请求不会导致每次请求更新会话表。