Files
ai-contract-platform/project-docs/V2-production-identity-email-security-2026-07-13.md
T
yuqei 7bc74f06f2
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 6m47s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 42m10s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m59s
release: freeze V2 production candidate
2026-07-20 18:39:27 +08:00

127 lines
7.0 KiB
Markdown
Raw 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.
# AI 合同平台 V2 生产身份交付与验证码安全闭环
日期:2026-07-13
## 本片结论
本片完成 SMTP、管理员线下开通、验证码限流和账号防枚举的代码闭环。当前首发不依赖企业邮箱:
- 本地实现与回归:完成。
- 数据库迁移:`20260713_0031`,空库升级、降级到 `20260710_0030`、再次升级均通过。
- `192.168.3.90` smoke 部署:已完成;迁移、无邮箱模式、管理员线下邀请、员工设密和登录闭环均已实测通过。
- 首发身份方式:`disabled + self-service=false + manual-invitation=true`。
- SMTP:代码保留,待取得企业邮箱后另行配置和实发验收,不阻塞当前受控上线。
### 无邮箱首发模式
- 登录保持可用。
- 平台管理员在 `/admin/identity` 创建公司租户和首个管理员。
- 租户管理员创建员工邀请,后台仅在创建时返回一次性邀请链接,并记录 `delivery_status=manual`。
- 公开公司注册、在线密码重置和邮箱验证关闭;前端隐藏入口,后端接口同时返回 `403`。
- 员工通过一次性邀请链接设置密码。账号访问问题暂由所属公司管理员线下处理。
- 首个生产平台管理员通过一次性 bootstrap 命令创建,密码只从标准输入或只读文件读取,不放进镜像、仓库或命令参数。
## 已实现能力
### 邮件交付
- 支持 SMTP、STARTTLS、SMTP over SSL、认证、连接超时、发件人名称和地址。
- SMTP 密码支持受保护环境变量,也支持容器挂载文件引用。
- 邮箱验证和密码重置验证码通过 SMTP 发送。
- 企业邀请链接通过 SMTP 发送;生产响应继续隐藏原始邀请 token。
- SMTP 发送在 HTTP 响应后执行,密码重置接口不会因已知账号需要等待网络发送而产生明显枚举时延。
- challenge 和 invitation 均记录 `queued / sent / failed / development / manual` 交付状态、尝试时间、成功时间和脱敏错误类型。
### 验证码安全
- 验证码仍只保存 SHA-256 哈希,生产响应不返回 `dev_code`。
- challenge 有效期限制为 5 至 30 分钟。
- 第 5 次错误输入立即落库为 `locked`;修复了旧实现异常回滚导致失败次数未持久化的问题。
- 重发冷却默认 60 秒。
- 默认 1 小时窗口:单邮箱 5 次、单 IP 20 次、单租户 100 次。
- 限流事件只保存使用独立 secret 计算的 HMAC 哈希,不保存原始 IP、邮箱或租户标识。
- PostgreSQL 使用按限流维度排序的事务级 advisory lock,避免多容器并发绕过计数。
### 防枚举与登录保护
- 已知邮箱、未知邮箱和未知租户的密码重置请求均返回同形 `200` 结果。
- 生产密码重置成功响应设置 120ms 基础时延和最多 30ms 随机抖动,进一步降低数据库路径差异带来的时序枚举信号。
- 登录的未知账号、错误密码、多租户歧义和禁用账号统一返回 `401` 与相同文案。
- 不存在账号和无密码凭据账号执行 dummy PBKDF2 校验,降低密码校验时延差异。
- 旧的 `legacy_email_session` 无密码登录路径已关闭;缺少凭据的历史账号需走密码重置。
- 登录默认 15 分钟窗口:单邮箱 10 次、单 IP 50 次;成功登录会清除该邮箱失败窗口,IP 窗口继续保留。
## 生产变量
当前无邮箱首发配置:
```text
CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE=disabled
CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED=false
CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED=true
CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET=LOAD_A_STABLE_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS
```
以后启用企业邮箱时切换为:
```text
CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE=smtp
CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED=true
CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED=false
CONTRACT_API_IDENTITY_SMTP_HOST=smtp.example.com
CONTRACT_API_IDENTITY_SMTP_PORT=587
CONTRACT_API_IDENTITY_SMTP_USERNAME=mailer@example.com
CONTRACT_API_IDENTITY_SMTP_PASSWORD=LOAD_FROM_PROTECTED_ENV
CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL=mailer@example.com
CONTRACT_API_IDENTITY_SMTP_FROM_NAME=AI 合同平台
CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS=true
CONTRACT_API_IDENTITY_SMTP_USE_SSL=false
CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS=10
CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL=https://contracts.example.com
CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET=LOAD_A_STABLE_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS
```
也可用 `CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE` 指向容器内只读 secret 文件。不要同时开启 STARTTLS 和 SSL。`IDENTITY_RATE_LIMIT_SECRET` 必须跨重启保持稳定,且不得与 smoke 共用。
完整默认限流变量见:
- `yuqei-contract-platform/services/contract-api/.env.example`
- `yuqei-infra/compose/.env.production.example`
## 反向代理要求
- 互联网入口必须覆盖客户端传入的 `X-Real-IP` / `X-Forwarded-For`,不能原样信任客户端伪造值。
- Contract Web 从受控入口头提取客户端 IP,并通过已认证的内部请求头 `X-Identity-Client-IP` 传给 Contract API。
- Contract API 仅在 `CONTRACT_API_REQUIRE_WEB_PROXY_AUTH=true` 时信任该内部头。
## 验证证据
- Contract API 全量测试:通过。
- 身份专项测试覆盖 SMTP 成功/失败、邀请投递、未知邮箱同形响应、邮箱/IP/租户限流、重发冷却、第五次错误锁定和生产验证码隐藏。
- Frontend `npm run typecheck`:通过。
- Frontend `npm run build`:通过,83 个路由生成成功。
- Bandit Contract API 全量扫描:0 告警。
- tracked-source detect-secrets:`new=0`。
- 新增身份文件 detect-secrets 单独扫描:0 候选。
## Smoke 验收清单
最新通过证据:
- Docker smoke:`project-docs/qa-artifacts/v2-smoke-docker-deploy/2026-07-16T11-12-47`
- 发布质量门:`project-docs/qa-artifacts/v2-release-quality-gate/2026-07-16T11-18-52`
- Contract API Alembic head:`20260713_0031`
- 无邮箱身份闭环:公开注册/找回/邮箱验证均为 `403`;管理员登录、线下邀请、员工设密和登录均通过。
1. 备份 `yuqei_contract`,部署候选镜像并运行 Alembic 到 `20260713_0031`。
2. `/api/v1/runtime/schema-check` 中 `identity_security_columns=ok`。
3. `/api/v1/runtime/config-check` 中 `identity_email_delivery=ok`、`identity_rate_limit=ok`。
4. 用只读 secret 文件运行管理员 bootstrap,并确认重复执行返回 `already_configured`。
5. 验证 `/identity/capabilities` 显示注册、密码重置和邮箱验证关闭,手工邀请开启。
6. 验证公开注册、密码重置和邮箱验证接口返回 `403`。
7. 平台管理员创建测试租户,租户管理员生成一次性邀请链接。
8. 用该链接设置员工密码并完成登录,确认邀请记录为 `manual` 且 token 只在创建响应出现。
9. 检查日志无验证码、邀请 token 或完整凭据泄露。SMTP 上线时再执行三类邮件实发和失败告警专项验收。
完成无邮箱 smoke 闭环后,当前身份阻断项可关闭;SMTP 验收作为未来启用邮件功能的独立准入项。