读完了两份 spec + 分派器,这分支的全貌清晰了。下面是可直接粘进 Gitea PR 的 MR 说明(标题 + 正文): 建议标题 feat(sms): 短信验证码可切换多 provider(极光/阿里云/创蓝),默认极光零改动 MR 正文(复制以下 markdown): ## 概述 把原单文件 `app/integrations/sms.py` 重构为**可切换 provider 包** `app/integrations/sms/`,在保留极光(默认、行为零改动)的基础上,新增两家验证码短信 provider: - **阿里云号码认证 dypns**(Mode A:阿里云生成/存储/校验验证码,核验免费) - **创蓝云智 253**(Mode B:本服务自管码,httpx 直连 + HMAC 签名) Provider 由 `SMS_PROVIDER` 按调用实时选择,默认 `jiguang`。短信=花钱 + 登录关键路径,故新 provider **opt-in、可灰度、秒级回退**,默认路径零变更。 ## 为什么 现有极光路径本地内存存码(多 worker 不共享,已是技术债),且单一供应商无法灰度/切换。引入 provider 抽象后:阿里云托管码可消除存码债,创蓝作为备选降低单点依赖,三家随配置切换与回退。 ## 改动内容 **架构(`app/integrations/sms/`)** | 文件 | 说明 | |---|---| | `__init__.py` | 对外仍暴露 `send_code/verify_code/SmsError`(auth 导入不变);按 `SMS_PROVIDER` **每次调用**分派;未知值回退 `jiguang` | | `base.py` | `SmsError`(status_code→HTTP) + provider 无关的 `mock_verify` | | `jiguang.py` | 原 `sms.py` 逻辑**原样迁入**,行为零改动(git 识别为 rename) | | `aliyun.py` | 新增,Mode A:`SendSmsVerifyCode` + `CheckSmsVerifyCode`,惰性加载 SDK | | `chuanglan.py` | 新增,Mode B:自管码 + `tpl/send` + HMAC 签名 | **两种验证码模式** - Mode A(阿里云):不本地存码,阿里云 `##code##` 托管生成+校验;本地仅留 per-phone 失败计数防爆破。 - Mode B(极光/创蓝):`secrets` 生成 N 位 → 进程内存 → 供应商只下发;本地一次性校验 + 失败 N 次作废。创蓝**复制**极光存码机器(不重构极光,零回归风险)。 **配置(`config.py` + `.env.example`)** - `SMS_PROVIDER = jiguang | aliyun | chuanglan`(默认 jiguang) - `ALIYUN_SMS_*`(AK/签名/模板/方案名/时长…) + `aliyun_sms_configured` 门控 - `CHUANGLAN_SMS_*`(账号/密码/模板/签名/endpoint…) + `chuanglan_sms_configured` 门控 - 复用现有 `SMS_MOCK / SMS_CODE_LENGTH / SMS_CODE_TTL_SEC / SMS_SEND_INTERVAL_SEC / SMS_MAX_VERIFY_ATTEMPTS` - 切到某 provider 却未配齐 → `send_code` 抛 `SmsError(503)`,不静默 **auth.py(最小改动)** - `verify_code` 现在可能抛 `SmsError`(阿里云降级 503)→ `sms_login`、`wechat_bind_phone_sms` 两处各包 `try/except SmsError → HTTPException`,与 `send_code` 现有写法一致。 **依赖** - `pyproject.toml` 增 `alibabacloud_dypnsapi20170525`(仅阿里云 provider 惰性 import;jiguang/chuanglan 不加载)。创蓝零新依赖(httpx + 标准库)。 **测试** - 新增 `test_sms_aliyun.py` / `test_sms_chuanglan.py`(均 monkeypatch 网络接缝,不发真短信) + `test_sms_dispatch.py`(分派/回退)。 - `test_auth.py` 相应更新。 - 现有测试走 `SMS_MOCK=true` 在分派层短路,不受影响。 **文档** - 设计 spec:`docs/superpowers/specs/2026-07-25-aliyun-sms-verify-design.md`、`2026-07-26-chuanglan-sms-verify-design.md` - 接口调研:`docs/integrations/aliyun/*`、`docs/integrations/chuanglan/tpl-send.md`、`docs/integrations/sms.md` ## 兼容性 & 回退 - **默认 `SMS_PROVIDER=jiguang`,线上行为与现状完全一致**;不改极光逻辑、不动 API 层频控与测试账号短路。 - 切阿里云/创蓝仅改环境变量,出问题秒切回极光;未知 `SMS_PROVIDER` 一律回退极光,防误配打挂登录。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #188
5.9 KiB
短信验证码(sms)
文件:
app/integrations/sms/(分派器__init__+jiguang/aliyunprovider +base) | 关联接口:auth-sms-send · auth-sms-login | ← 集成索引
作用
手机号 + 验证码登录的验证码发送 / 校验。支持可切换 provider(SMS_PROVIDER):jiguang(默认,极光自管码)/ aliyun(阿里云号码认证托管码)/ chuanglan(创蓝云智模板短信,自管码)。SMS_MOCK 另切 mock / real。
短信提供商(SMS_PROVIDER,可切换 + 灰度回退)
jiguang(默认) |
aliyun |
chuanglan |
|
|---|---|---|---|
| 验证码模式 | 自管码 Mode B | 托管码 Mode A | 自管码 Mode B(与极光同) |
| 验证码 | 本服务生成、极光只下发、本地内存校验 | 阿里云生成 + 下发 + 校验(dypns 号码认证,核验免费) | 本服务生成、创蓝只下发、本地内存校验 |
| 发码 | 极光 /v1/messages |
SendSmsVerifyCode(##code## 占位) |
创蓝 tpl/send v2(HMAC 签名头,password 不上行) |
| 校验 | 比对本地存码 | CheckSmsVerifyCode → PASS / UNKNOWN |
比对本地存码 |
| 多 worker | ⚠️ 内存存码不共享(见已知局限) | ✅ 阿里云托管,天然共享 | ⚠️ 内存存码不共享(与极光同级债) |
| 防爆破 | 单码失败 SMS_MAX_VERIFY_ATTEMPTS 次即作废 |
同语义(本地 per-phone 失败计数) | 同语义(复制自极光) |
| 单号冷却 | 本地 SMS_SEND_INTERVAL_SEC |
交给阿里云 Interval |
本地 SMS_SEND_INTERVAL_SEC |
| 依赖 | httpx(复用) |
SDK alibabacloud_dypnsapi20170525 |
httpx + 标准库 hashlib/hmac(无新依赖) |
aliyun需在号码认证控制台开通「融合认证」,用系统赠送签名 + 赠送模板;配置见.env.example的ALIYUN_SMS_*,SDK 为alibabacloud_dypnsapi20170525。chuanglan用 YZM 前缀验证码账号,服务器出网 IP 须在创蓝控制台加白名单(否则 117);Mode B 存码/冷却/校验逻辑是从极光隔离复制(极光文件不动),仅发送走 HMAC 签名。配置见.env.example的CHUANGLAN_SMS_*,接口调研见 chuanglan/tpl-send.md,设计见 spec。
以下章节描述 jiguang provider(自管码)细节(chuanglan 的存码/冷却/校验语义与之相同)。
mock(SMS_MOCK=true,默认 / 开发测试) |
real(SMS_MOCK=false,生产) |
|
|---|---|---|
| 发送 | 不真发,验证码打日志 | 调极光 /v1/messages 真发 |
| 校验 | 放行任意 6 位数字(测试便利) | 比对本服务存的码(一次性 / 过期 / 防爆破) |
为什么是"自己生成码"模式
极光有两种验证码玩法:① /v1/codes 极光生成 + /codes/{msg_id}/valid 极光校验;② /v1/messages 本服务生成码、极光只负责发。选 ②:校验本地完成(不依赖极光二次往返)、有效期/错误次数/作废完全可控、跟原 mock 口子结构一致。
函数 / 异常
| 函数 | 行为 |
|---|---|
send_code(phone) -> int |
防刷检查(冷却)→ secrets 生成 N 位码 → 预占(冷却/存码)→ mock 打日志 / real 调极光 → 失败回滚预占。返回距下次可发秒数 |
verify_code(phone, code) -> bool |
mock 放行任意 6 位;real 比对存码,匹配即作废,失败累计到上限作废 |
SmsError(msg, status_code) |
status_code 决定 HTTP 码:过频 429、供应商失败 503、号码无效 400 |
配置
| 配置项 | 默认 | 说明 |
|---|---|---|
SMS_MOCK |
true |
mock / real 切换。生产置 false |
SMS_CODE_TTL_SEC |
300 | 验证码有效期(秒),与极光模板文案"5分钟"一致 |
SMS_SEND_INTERVAL_SEC |
60 | 单号两次发送最小间隔(冷却) |
SMS_SEND_ENDPOINT |
极光 /v1/messages |
短信发送地址 |
SMS_SIGN_ID |
31729 | 极光签名 ID |
SMS_TEMPLATE_ID |
1 | 极光模板 ID(变量名 code) |
SMS_CODE_LENGTH |
6 | 验证码位数(本服务生成;前端 code 4-8 位兼容) |
SMS_MAX_VERIFY_ATTEMPTS |
5 | 单码最多校验失败次数,超过作废(防爆破) |
鉴权复用极光一键登录:
/v1/messages用base64(JG_APP_KEY:JG_MASTER_SECRET)做 HTTP Basic Auth——短信与一键登录是同一个极光应用(同 AppKey)。上线不需要额外凭证,只需SMS_MOCK=false(JG_*一键登录已配)。
防刷(短信花钱 + /sms/send 在登录前无法 JWT 鉴权)
2026-07-03 精简:登录风控只留「单号冷却 + 单设备频控」两道主策略(删单号每日上限、删登录纯 IP
rate_limit);单码失败上限属验证码安全底线,保留。
- 单号
SMS_SEND_INTERVAL_SEC冷却(单号维度) - 单设备(
device_id+ IP)每小时频控:/sms/send≤SMS_SEND_MAX_PER_HOUR_PER_DEVICE(5)、/sms/login≤SMS_LOGIN_MAX_PER_HOUR(5)——堵「换号绕开单号冷却」+ 挡登录撞库,在app/api/v1/auth.py内enforce_rate_limit - 单码校验失败
SMS_MAX_VERIFY_ATTEMPTS次即作废 + 验过即作废(一次性) - 运维侧建议在极光控制台叠加:IP 白名单(只许服务器 IP)+ 防轰炸设置
极光错误码(节选,映射在 _send_via_jiguang)
| code | 含义 | 处理 |
|---|---|---|
| 50014 | 余额不足 | logger.critical 告警 + 503 |
| 50009 | 极光侧超频 | 429 |
| 50006 | 手机号无效 | 400 |
| 其他 | — | 503 |
上线步骤
- 极光控制台:企业实名 + 短信充值 + 签名审核(
sign_id=31729)+ 模板审核(temp_id=1)——均已就绪 .env设SMS_MOCK=false(JG_APP_KEY/JG_MASTER_SECRET一键登录已配)- 真机发一条验证:收到短信 + 能登录
已知局限
验证码存进程内存:单 worker uvicorn 够用;重启丢码(用户重发即可);多 worker / 多机不共享 → 冷却 / 校验失效,扩 worker 前迁移到 DB/Redis。见 待办与技术债。