Files
meida/docs/开发任务包/T11-短信登录代码先行.md
T
xsl e511b6a66f 收尾两批并行开发的文档回写与任务包
- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
2026-08-14 21:07:20 +08:00

102 lines
7.4 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.
# T11 · 登录·手机号验证码(代码先行,联调后置)
> 对应任务:`T-P1-12b/12c` | 需求:`FR-ACC-04`(6 位、5 分钟、一次性、失败 5 次作废)、`FR-ACC-14`(三重频控防刷)、`TD-32` | 分支:`task/t11-auth-sms` | worktree:`/home/xsl/meida_backend-t11`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
手机号验证码登录全链路(发送 → 校验 → 建户 → 双令牌)+ **短信轰炸防护**。腾讯云短信签名/模板还在审核(《前置开通清单》§1),因此真实发送后置:`SmsSender` 走 Protocol 隔离,默认 `fake` 实现(验证码打日志,dev 可用),腾讯实现写好待凭证。
**FR-ACC-04 纪律:与华为通道不得互相依赖**——本任务不 import 华为相关任何东西,登录成功路径只依赖 identity 域已有设施。
## 2. 已就绪的契约(协调提交已提供)
### 配置(config.py 已加,.env.example 已示例)
```python
sms_provider: str = "fake" # SMS_PROVIDER: fake | tencent
tencent_sms_secret_id/key/sdk_app_id/sign_name/template_id: str = ""
sms_code_ttl_seconds: int = 300 # 5 分钟(FR-ACC-04)
sms_code_length: int = 6
sms_verify_max_attempts: int = 5 # 校验失败 5 次作废
sms_resend_cooldown_seconds: int = 60 # 同号 60s 内 1 条
sms_hourly_limit: int = 5 # 同号 1h ≤5
sms_daily_limit: int = 10 # 同号 24h ≤10
sms_ip_daily_limit: int = 20 # 同 IP 24h ≤20
sms_device_daily_limit: int = 10 # 同设备 24h ≤10
```
### 错误码(errors.py 已加,含 meta)
- `SMS_CODE_INVALID = 40110`(401「验证码错误」hint「请检查后重试」)
- `SMS_CODE_EXPIRED = 40111`(401「验证码已过期」hint「请重新获取」)
- `SMS_SEND_TOO_FREQUENT = 42910`(429「发送太频繁了」hint「请稍后再试」)
- `SMS_DAILY_LIMIT = 42911`(429「今日发送次数已达上限」hint「明天再试」)
- `SMS_PROVIDER_FAILED = 61000`(502「短信服务暂时不可用」)
### 依赖与设施
- `tencentcloud-sdk-python-sms` 已装(协调提交)。
- 可复用(先读再写):`packages/identity/service.py` 的 `get_or_create_by_external_identity`(provider 用 `"phone"`)与 `issue_session`(T4);`packages/identity/schemas.py` 的 `TokenPairResponse`——**复用,不重复定义、不修改该文件**。
- 路由骨架:`apps/api/routers/auth_sms.py` 空路由(prefix `/auth`)已注册 main.py——归你填充。`apps/api/routers/auth.py` 归 T10,**禁碰**。
### 号码存储决策(写进代码注释)
V1 简化:`id_user_identity` 现有列无 `phone_enc`,用 `union_id = "phone:" + sha256(规范化号码)[:16]`(E.164 规范化:去 +86/前导零/空白后哈希)。**Redis 与日志中一律不存明文号码**——键用 `sha256(phone)` 前 16 位,日志打码 `138****1234`(FR-ACC-14 掩码要求)。`phone_enc` 列的补齐属后续迁移任务,不在本批。
## 3. 实现要点
允许改动:`packages/identity/sms_login.py`(新建,全部核心逻辑)、`apps/api/routers/auth_sms.py`、测试文件。**不改 identity/{service,repo,schemas,tokens,huawei}.py。**
1. **`packages/identity/sms_login.py`**:
- `SmsSender` Protocol:`async def send_code(phone_masked_key: str, code: str) -> None`(收掩码键不收明文——防实现层记日志泄漏);
- `FakeSmsSender`:`logger.info("sms_code_issued", masked=..., code=...)`(dev 专用,注释写明生产必须切 tencent);
- `TencentSmsSender`:包官方 SDK(`SmsClient` + `SendSmsRequest`,参数从 settings 读);构造失败/响应非 ok → `AppError(SMS_PROVIDER_FAILED)`;**本批不要求真实联调,但代码路径要完整可读**;
- `SmsAuthService(redis, sender, *, settings)`:
- `request_code(phone, ip, device_id)`:号码格式校验(大陆手机号正则 `^1[3-9]\d{9}$`,写死并注释 V1 只做大陆)→ **先频控后发送**:同号 cooldown/hourly/daily、IP daily、设备 daily 全过才生成 6 位随机码(`secrets.randbelow`,非 random)→ 存 Redis `sms:code:{phone_hash}`(值 = sha256(code),**不存明文**,TTL 300s)+ 计数器自增 → sender 发送;
- 频控计数键按自然窗口(cooldown 用 TTL;hourly/daily 用 UTC+8 小时/日期桶,对齐 billing 的时区纪律);
- `verify(phone, code, ip, device_id)`:取码哈希比对(**恒定时间比较** `hmac.compare_digest`)→ 错误则 attempts+1(`sms:attempts:{phone_hash}`,达 5 删码并返回 40110)→ 成功删码(一次性)→ 由调用方建户;
- 全部日志打码:`masked_phone(phone) -> "138****1234"`。
2. **路由**(auth_sms.py):
- `POST /auth/sms/send` body `{phone, device_id}`(ip 从 `Request.client.host` 取)→ 限流错误码透传;
- `POST /auth/sms/verify` body `{phone, code, device_id}` → 成功后 `get_or_create_by_external_identity(provider="phone", union_id=..., register_source="phone")` + `issue_session` → `ok(TokenPairResponse(...))`;
- 请求/响应模型定义在 sms_login.py 或新建 `packages/identity/sms_schemas.py`(二选一,不碰共享 schemas)。
3. **刷量测试**(FR-ACC-14 验收原文"脚本化刷量测试证明拦截有效"):集成测试用循环模拟。
## 4. 测试要求
- 单测 `tests/unit/test_sms_login.py`(fakeredis 或自写内存 dict 实现 Protocol 化的 redis 接口——参考 `InMemoryCostCounter` 的做法,不引新依赖):
1. 号码正则(非法格式 400 类错误);
2. 频控矩阵:cooldown 内二次 → 42910;1h 第 6 条 → 42910;24h 第 11 条 → 42911;IP 第 21 条 / 设备第 11 条 → 42911;
3. 验证码:错误 4 次可再试、第 5 次后码作废(40110 且码已删)、过期(TTL 模拟)→ 40111、成功后码即失效(二次 verify 40111);
4. 掩码函数;恒定时间比较(不 assert 时长,只 assert 行为)。
- 集成 `tests/integration/test_auth_sms.py`(新文件,SMS_PROVIDER=fake,从响应前的日志拿不到码——**测试钩子**:fake sender 把码写进 Redis 测试键或直接用 fixture 替换 sender 为捕获型 fake,不要为测试开后门到生产代码):
1. send → verify(fixture 捕获的码)→ 200 双令牌,新 access 可用;
2. 同号再登录同 user_id(映射唯一);
3. **刷量脚本化**:连发 11 次,第 11 次 42911;换 IP 维度再验一轮;
4. 错误码 5 次作废路径。
## 5. 禁改清单
通用禁改之外:`packages/identity/{service,repo,schemas,tokens,huawei}.py`、`apps/api/routers/auth.py`、`packages/{kb,rules,retrieval}/**`。
## 6. 验收标准(FR-ACC-04 / FR-ACC-14)
1. 验证码 6 位、5 分钟、一次性、失败 5 次作废——四点各有断言;
2. 三重频控全部生效且有脚本化刷量测试证明;
3. 号码与验证码在日志/Redis 均无明文(掩码 + 哈希);
4. 与华为通道零耦合(不 import huawei 模块);
5. 全部检查绿(mypy 范围同 T6 列表)。
## 7. 提交信息
```
feat(identity): phone SMS login with triple rate limiting and anti-abuse
SmsSender Protocol(fake/tencent)+ 验证码生命周期(哈希存储/一次性/
5 次作废)+ 号码-IP-设备三重频控 + 掩码纪律(FR-ACC-04/14,TD-32;
真实发送待签名审核)。
偏差说明:<如有写在这里,无则写"无">
```