# 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 架构交付) | 梁思筑 |