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

7.4 KiB
Raw Blame History

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 已示例)

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;
真实发送待签名审核)。

偏差说明:<如有写在这里,无则写"无">