Appearance
OAuth 限流规则(客户端如何优雅共处)
📍 来源:OAuth 文档
rate-limits.html服务端实现:Redis ZSET 滑动窗口(OAuth 实施后端 fastapi-limiter 三层封装) 所有限流触发统一报40101005(msg 文案区分具体维度)
1. 参数总表(2026-09-10 生产口径)
| # | 维度 | 窗口 | 上限 | 触发后 msg 关键词 |
|---|---|---|---|---|
| 1 | 图形验证码获取 · 每 IP | 60s | 30 次 | 「验证码获取过于频繁」 |
| 2 | 短信发送 · 每 IP | 60s | 30 条 | 「请求过于频繁」 |
| 3 | 短信发送 · 每手机号 | 60s | 1 条 | 「发送间隔过短,请 60 秒后再试」 |
| 4 | 短信发送 · 每手机号 | 24h | 10 条 | 「今日发送次数已达上限」 |
| 5 | 云端频率限制(腾讯云侧兜底) | 30s / 1h / 24h | 1 / 5 / 10 条 | 短信下发失败(非 40101005) |
⚠️ 腾讯云控制台侧还配了日阈值(预警 300 / 停发 500 条/天,全应用维度)—— 超量日全应用停发,前端只会看到
40101006。
2. 三层限流的精确语义
2.1 IP 维度(2 层)
limit: 60s/30 次
key: oauth:rate:sms:ip:{ip_subnet}:60s- 任何 IP 60s 内超过 30 次 → 40101005
- 同一 IP 给不同手机号发短信也累加
- 真机换网(WiFi↔4G)IP 变化 → 计数器重置
2.2 手机号 60s/1 条
limit: 60s/1 条
key: oauth:rate:sms:phone:{phone}:60s- 同一手机号 60s 内只能发送 1 条(不管 captcha 是否正确)
- 这是「60s 倒计时」按钮的硬约束
- 触发后 msg 含「60 秒」
2.3 手机号 24h/10 条
limit: 86400s/10 条
key: oauth:rate:sms:phone:{phone}:24h- 同一手机号 24h 内最多发 10 条(不论是否成功下发)
- 触发后 msg 含「今日次数」
- 触发后按钮置灰至次日
3. 前端倒计时设计(与限流参数对齐)
3.1 按钮状态机
idle ──点击──→ sending ──成功──→ counting(60s) ──→ idle
│失败
└→ idle(需重新拉图形验证码)counting期间按钮:disabled="countdown > 0"- 文案:
{n}s 后重发 - 「重新发送」超过 2 次后提示剩余次数(本地无法精确,按 10 条/天提示「今日次数有限」)
3.2 实现代码
typescript
const countdown = ref(0);
let timer: number | undefined;
function startCooldown(sec = 60) {
countdown.value = sec;
timer = setInterval(() => {
if (--countdown.value <= 0) clearInterval(timer);
}, 1000);
}
onUnmounted(() => clearInterval(timer));⚠️ 60s 倒计时是硬约束(服务端 60s/1 条),不要做成可跳过。 ⚠️ 同一手机号第 3 次发送(同分钟内)必然 40101005 —— 倒计时未结束就不该可点。
4. 429 优雅降级范式
typescript
if (err.code === 40101005) {
// msg 含「60 秒」→ 重置本地倒计时为 60s
if (err.msg.includes('60 秒')) {
startCooldown(60);
uni.showToast({ title: err.msg, icon: 'none' });
return;
}
// msg 含「今日」→ 按钮置灰至次日,文案「今日发送次数已达上限」
if (err.msg.includes('今日')) {
disableButtonUntilTomorrow();
uni.showToast({ title: err.msg, icon: 'none' });
return;
}
// 其余(IP 维度)→ 提示「操作过于频繁,请稍后再试」,指数退避
showExponentialBackoff(err.msg);
}| msg 关键词 | 动作 |
|---|---|
| 「60 秒」 | 重置倒计时 60s |
| 「今日」 | 按钮置灰至次日 + 文案「今日发送次数已达上限」 |
| 「IP」/「验证码获取过于频繁」 | 指数退避(1s / 2s / 4s / 8s) |
5. 防刷联运(前端配合项)
- 图形验证码是第一道闸:不要做「跳过验证码」的任何开关
captchaCode不区分大小写,但仍要禁止自动填充(input加autocomplete="off")- 检测到设备 root / 模拟器等风险特征时(如有风控 SDK),建议登录按钮加二次校验 —— 预留,服务端暂不强制
6. 后端实现(fastapi-limiter 三层封装)
python
# src/app/oauth/infrastructure/adapters/rate_limiter_redis.py
from fastapi_limiter.depends import RateLimiter
from src.app.oauth.domain.value_objects.phone import Phone
class RateLimiterRedisAdapter(RateLimiterPort):
"""三层限流封装:IP + 手机号 60s + 手机号 24h"""
async def check_three_layers(self, phone: Phone, client_ip_subnet: str) -> None:
# 1️⃣ IP 60s/30 次
ip_ok = await self._check_limit(
key=f"oauth:rate:sms:ip:{client_ip_subnet}",
limit=30, window=60,
)
if not ip_ok:
raise OAuthError(OAuthErrorCode.RATE_LIMITED, "请求过于频繁")
# 2️⃣ 手机号 60s/1 条
phone_60s_ok = await self._check_limit(
key=f"oauth:rate:sms:phone:{phone.value}:60s",
limit=1, window=60,
)
if not phone_60s_ok:
raise OAuthError(
OAuthErrorCode.RATE_LIMITED,
"发送间隔过短,请 60 秒后再试",
)
# 3️⃣ 手机号 24h/10 条
phone_24h_ok = await self._check_limit(
key=f"oauth:rate:sms:phone:{phone.value}:24h",
limit=10, window=86400,
)
if not phone_24h_ok:
raise OAuthError(
OAuthErrorCode.RATE_LIMITED,
"今日发送次数已达上限",
)Why msg 区分:前端按 msg 关键词做不同 UI 动作(按钮置灰 vs 倒计时 vs 退避)。
7. 联调检查清单
- [ ] 同一手机号 60s 内第 2 次
sms-send→ 40101005(msg 含「60 秒」) - [ ] 同一手机号 24h 内第 11 次
sms-send→ 40101005(msg 含「今日」) - [ ] 同一 IP 60s 内第 31 次
captcha→ 40101005(msg 含「验证码获取过于频繁」) - [ ] 腾讯云凭据未配置时
sms-send→ 40101006(不是 40101005) - [ ] 倒计时期间按钮 disabled(前端不应让用户能点)
- [ ] 倒计时归零后按钮可点击(避免永久禁用)