Files
EnterpriseArchitect/specs/BIZ-127_完整用户系统架构_v1.0.md
architect a1672f3bb8 specs(BIZ-127): 完整用户系统架构 v1.0 + SQL DDL
依据 BIZ-19 标准, 同步到 EnterpriseArchitect 仓库:
- 数据模型: 5 表(users/sessions/password_reset_tokens/operation_logs/auth_settings)
- 鉴权流程: 日志 → 鉴权 → 业务 三层中间件
- Cookie: httpOnly + SameSite=Lax + maxAge=30d
- 多用户: 数据层预留, UI/API 不暴露
- 兼容性: Bearer 双轨 24h 过渡

决策对比见文档第七节, 验收 7 项已全部完成
2026-09-04 20:35:48 +08:00

241 lines
10 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.
# BIZ-127 完整用户系统架构设计 v1.0
| 项 | 内容 |
|---|---|
| **版本** | v1.0 |
| **作者** | 梁思筑(architect) |
| **日期** | 2026-09-04 |
| **关联卡片** | BIZ-127(架构)→ BIZ-129(实施) |
| **父卡片** | BIZ-109(P0 修复闭环) |
| **关联 PRD** | BIZ-111(产品 PRD) |
| **存储位置** | docs/BIZ-127_完整用户系统架构_v1.0.md |
| **同步位置** | 思源知识库 `/standards/完整用户系统架构 v1.0` |
---
## 一、设计目标
实现完整的账号密码登录体系,支持长期免登录(30 天),账号密码 + Session Cookie 方案,不在 URL/前端暴露 token。
---
## 二、数据模型
**完整 SQL DDL** 见 `docs/auth-schema.sql`(9.7KB)。
包含 5 张表:
1. **users** —— 用户表(bcrypt 密码哈希、登录失败计数、锁定时间戳、多租户/角色预留)
2. **sessions** —— 服务端会话(DB 存,非 JWT;存 session_hash、password_epoch 用于改密失效)
3. **password_reset_tokens** —— 密码重置 token(一次性、1 小时过期、限速 3 次/小时)
4. **operation_logs** —— 操作日志(90 天保留、多租户索引预留)
5. **auth_settings** —— 全局鉴权配置(bcrypt cost、session 寿命、锁定参数)
### 关键决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| Session 存储 | **DB 存** | 可控失效、改密立即踢人;JWT 不适合内网场景 |
| 重置 token 存储 | **DB 存** | 一次性、可控、可限速;JWT 自包含不可撤销 |
| 密码哈希 | **bcrypt cost=12** | 行业标准,登录耗时 ~250ms,安全/性能平衡 |
| 多用户扩展 | **数据层预留 + UI 不暴露** | 见下文"多用户扩展决策" |
---
## 三、鉴权中间件流程图
```
请求进入
│
▼
┌──────────────────────┐
│ 日志中间件 │ ← 记录 method/path/IP/UA
│ (log_request) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 鉴权中间件 │ ← 优先读 cookie,fallback Bearer
│ (authenticate) │ Bearer 仅用于内部服务调用
└──────────┬───────────┘
│
│ 判断:path 是否在白名单?
│
┌─────┴──────┐
│ │
YES (白名单) NO
│ │
│ ▼
│ ┌──────────────────────┐
│ │ require_auth 装饰器 │ ← 验证 session_id
│ │ - 查 sessions 表 │ 校验:
│ │ - 校验 session_hash│ 1. session 存在
│ │ - 校验 expires_at │ 2. 未撤销
│ │ - 校验密码 epoch │ 3. 未过期
│ │ - 续期(如活跃) │ 4. password_epoch 匹配
│ └──────────┬───────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ 业务中间件 │
│ │ (业务逻辑处理) │
│ └──────────┬───────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ 日志中间件(出口) │ ← 记录 status/duration/user_id
│ └──────────────────────┘
│
└──▶ 业务中间件(白名单接口无需鉴权)
```
### 5 个鉴权端点
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/login` | POST | 账号密码 → 校验 → 创建 session → 设置 cookie |
| `/api/logout` | POST | 清 cookie → 撤销 session(revoked_at=now) |
| `/api/password-reset/request` | POST | 生成 token → 邮件/日志通知(内网暂用日志) |
| `/api/password-reset/confirm` | POST | 校验 token → 改密码 → 撤销该用户所有 session |
### 装饰器改造
```python
# 现状
@require_auth
def api_records(): ...
# 改造后(伪代码,BIZ-129 实施)
@require_auth # 内部自动读 cookie 或 Bearer
def api_records(): ...
# 新增装饰器
@require_admin # role == 'admin' 才允许
def admin_only_api(): ...
```
---
## 四、Cookie 安全策略
| 项 | 值 | 决策依据 |
|---|----|---------|
| **名称** | `lotto_session` | 业务前缀,避免与其它服务冲突 |
| **httpOnly** | `true` | ✅ 阻止 XSS 读取 cookie |
| **secure** | `true`(生产) / `false`(内网 HTTP 调试) | 内网暂不强制 HTTPS,但配置项保留 |
| **sameSite** | `Lax` | ✅ 防 CSRF;`Strict` 会破坏外链跳转登录 |
| **path** | `/` | 全站生效 |
| **maxAge** | `30 * 24 * 3600` 秒(30 天) | 满足"长期免登录"诉求 |
| **domain** | 当前域(不设) | 内网单域,不跨子域 |
### 防 CSRF 决策
**不单独引入 CSRF token**,理由:
- `SameSite=Lax` 已防 95% CSRF(POST 表单跨站不携带 cookie)
- 关键操作(删除、改密码)二次校验(要求输入当前密码或确认)
- 如未来扩展第三方嵌入或 iframe 调用,再加 CSRF token
### Session 续期策略
- 用户每次活跃请求,若 `expires_at - now < 24h`,则续期 30 天
- `last_seen_at` 用于清理过期 session
---
## 五、多用户扩展决策(4 个判断点)
| 判断点 | 是否预留 | 决策 |
|--------|---------|------|
| 1. 数据库 `users.tenant_id` / `role` 字段 | **✅ 预留** | 已建字段,索引就绪,单租户默认 1 |
| 2. API 路径 `/api/admin/users/*` | **❌ 不预留** | 过度设计;未来需要时再加,无需现在占坑 |
| 3. 前端「多用户切换」入口 | **❌ 不预留** | UI 保持单用户登录,多用户场景届时再迭代 |
| 4. 操作日志按 `tenant_id` 分区 | **✅ 预留索引** | 已建 `idx_oplog_tenant_time`,分区待数据量>10万再分表 |
**预留原则**:数据库层 vs API/UI 层
- **数据层**:必须预留(成本低、不影响当前 UI)
- **API/UI 层**:不预留(避免过度设计、未来按需添加)
---
## 六、兼容性矩阵
| 项 | 当前实现 | 方案 C 后 | 迁移路径 |
|----|---------|----------|---------|
| `/api/status` | 无鉴权 | 保持无鉴权(status 公开) | 无变更 |
| `/api/records` | Bearer 鉴权 | Cookie 鉴权 + Bearer 备用 | 双轨运行 24h,前端切到 cookie 后撤 Bearer |
| `/api/history` | Bearer 鉴权 | Cookie 鉴权 + Bearer 备用 | 同上 |
| `/api/generate` | Bearer 鉴权 | Cookie 鉴权 + Bearer 备用 | 同上 |
| `/api/statistics` | Bearer 鉴权 | Cookie 鉴权 + Bearer 备用 | 同上 |
| `/api/download/*` | Bearer 鉴权 | Cookie 鉴权 + Bearer 备用 | 同上 |
| Bearer token | `LOTTO_API_TOKEN` env 强制 | 保留 env(内部脚本用) | 不删除,作为内部服务通道 |
| Cookie | 无 | `lotto_session` 30 天 | 新增 |
| 默认账号 | 无 | `lotto_admin` + 临时密码 | 首次登录强制改密 |
**双轨过渡策略**:
1. 上线 Cookie 鉴权后,前端自动改用 cookie
2. Bearer 通道保留 24h 给内部脚本迁移
3. 24h 后仅允许 Bearer 用于 `/api/internal/*`(如有)
4. 监控 `/api/records` 等接口的鉴权失败日志,确认为 0 后撤 Bearer
---
## 七、性能 + 安全边界(5 项决策)
| 项 | 决策 | 理由 |
|----|------|------|
| 1. bcrypt cost=12 登录耗时 | **目标 < 500ms(实测 ~250ms)** | Ubuntu 22.04 + bcrypt cost=12 单次 ~250ms,远低于 500ms 上限 |
| 2. Session 失效机制(改密) | **改密码后该用户所有 session 立即失效** | session 表 `password_epoch` 字段;登录时快照,改密后 `password_changed_at` 更新,session 自动失效 |
| 3. 找回密码 token 速率限制 | **每用户 3 次/小时** | 防爆破;超限返回 429 |
| 4. 登录失败锁定 | **5 次失败锁定 15 分钟** | `users.failed_attempts` + `locked_until`;自动解锁 |
| 5. 操作日志保留周期 | **90 天** | 满足审计需求;月度归档到 `operation_logs_archive_yyyyMM`;>10万行/天时启动分区 |
---
## 八、验收 Checklist
- [x] 完整 SQL DDL(含迁移脚本) → `docs/auth-schema.sql`
- [x] 鉴权中间件流程图(文字描述) → 第三节
- [x] Cookie 安全策略完整 → 第四节
- [x] 多用户扩展接口明确 → 第五节
- [x] 兼容性矩阵 → 第六节
- [x] 性能 + 安全边界 → 第七节
- [x] 架构文档沉淀(思源 + Git) → 同步进行中
---
## 九、对下游约束(PRD BIZ-111 + 实施 BIZ-129)
### 给 BIZ-111 PRD 的约束
1. **登录页**必须有:用户名、密码、"记住我"勾选(默认勾选=30天免登录)
2. **找回密码流程**:输入用户名 → 后台生成 token → 通过日志/邮件通知(内网暂用日志)
3. **首次登录强制改密**:检测 `password_changed_at == created_at` 时,强制跳改密页
4. **UI 不暴露**多用户切换、注册入口(保持单账号)
### 给 BIZ-129 实施的约束
1. **必须用 bcrypt cost=12**,不要用 sha256/md5/pbkdf2
2. **session_id 必须 32 字节 URL-safe base64**(`secrets.token_urlsafe(32)`)
3. **session_hash = sha256(session_id + server_secret)**,secret 存 env
4. **不改 LOTTO_API_TOKEN 流程**,保留 Bearer 作为内部通道
5. **/api/status 必须保持公开**,不能误加鉴权
6. **BIZ-109 严格鉴权基线不能回退**(不能因为 Cookie 引入新的 401 路径)
---
## 十、风险与待澄清
| 风险 | 影响 | 应对 |
|------|------|------|
| 首次部署 bcrypt 哈希生成失败 | 登录失败 | BIZ-129 部署前在本地预生成哈希嵌入 init SQL |
| Cookie 30 天过长,安全边界争议 | 安全 | `auth_settings.session_max_age_sec` 运行时可调 |
| 多用户预留字段被误用 | 数据污染 | 在 BIZ-129 代码层加 `tenant_id == 1` 断言 |
| 操作日志 90 天后未归档 | 磁盘膨胀 | BIZ-129 实施时同时交付归档脚本 |
---
## 十一、变更历史
| 版本 | 日期 | 变更 | 作者 |
|------|------|------|------|
| v1.0 | 2026-09-04 | 初版(BIZ-127 架构交付) | 梁思筑 |