- 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 中相应引用改为留痕表述
7.4 KiB
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。
packages/identity/sms_login.py:SmsSenderProtocol: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)→ 存 Redissms: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"。
- 路由(auth_sms.py):
POST /auth/sms/sendbody{phone, device_id}(ip 从Request.client.host取)→ 限流错误码透传;POST /auth/sms/verifybody{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)。
- 刷量测试(FR-ACC-14 验收原文"脚本化刷量测试证明拦截有效"):集成测试用循环模拟。
4. 测试要求
- 单测
tests/unit/test_sms_login.py(fakeredis 或自写内存 dict 实现 Protocol 化的 redis 接口——参考InMemoryCostCounter的做法,不引新依赖):- 号码正则(非法格式 400 类错误);
- 频控矩阵:cooldown 内二次 → 42910;1h 第 6 条 → 42910;24h 第 11 条 → 42911;IP 第 21 条 / 设备第 11 条 → 42911;
- 验证码:错误 4 次可再试、第 5 次后码作废(40110 且码已删)、过期(TTL 模拟)→ 40111、成功后码即失效(二次 verify 40111);
- 掩码函数;恒定时间比较(不 assert 时长,只 assert 行为)。
- 集成
tests/integration/test_auth_sms.py(新文件,SMS_PROVIDER=fake,从响应前的日志拿不到码——测试钩子:fake sender 把码写进 Redis 测试键或直接用 fixture 替换 sender 为捕获型 fake,不要为测试开后门到生产代码):- send → verify(fixture 捕获的码)→ 200 双令牌,新 access 可用;
- 同号再登录同 user_id(映射唯一);
- 刷量脚本化:连发 11 次,第 11 次 42911;换 IP 维度再验一轮;
- 错误码 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)
- 验证码 6 位、5 分钟、一次性、失败 5 次作废——四点各有断言;
- 三重频控全部生效且有脚本化刷量测试证明;
- 号码与验证码在日志/Redis 均无明文(掩码 + 哈希);
- 与华为通道零耦合(不 import huawei 模块);
- 全部检查绿(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;
真实发送待签名审核)。
偏差说明:<如有写在这里,无则写"无">