Skip to content

OAuth 限流规则(客户端如何优雅共处) ​

📍 来源:OAuth 文档 rate-limits.html 服务端实现:Redis ZSET 滑动窗口(OAuth 实施后端 fastapi-limiter 三层封装) 所有限流触发统一报 40101005(msg 文案区分具体维度)


1. 参数总表(2026-09-10 生产口径) ​

#维度窗口上限触发后 msg 关键词
1图形验证码获取 · 每 IP60s30 次「验证码获取过于频繁」
2短信发送 · 每 IP60s30 条「请求过于频繁」
3短信发送 · 每手机号60s1 条「发送间隔过短,请 60 秒后再试」
4短信发送 · 每手机号24h10 条「今日发送次数已达上限」
5云端频率限制(腾讯云侧兜底)30s / 1h / 24h1 / 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. 防刷联运(前端配合项) ​

  1. 图形验证码是第一道闸:不要做「跳过验证码」的任何开关
  2. captchaCode 不区分大小写,但仍要禁止自动填充(input 加 autocomplete="off")
  3. 检测到设备 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(前端不应让用户能点)
  • [ ] 倒计时归零后按钮可点击(避免永久禁用)