Initial commit

This commit is contained in:
wangxuming
2026-07-12 15:53:24 +08:00
commit 68d61700d5
252 changed files with 23291 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# 登录持久化设计
## 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 会统一返回登录页。
- 并发请求不会导致每次请求更新会话表。