- 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 中相应引用改为留痕表述
102 lines
7.4 KiB
Markdown
102 lines
7.4 KiB
Markdown
# 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;
|
||
真实发送待签名审核)。
|
||
|
||
偏差说明:<如有写在这里,无则写"无">
|
||
```
|