---
url: /docs/api-docs/v1/oauth/rate-limits.md
---
# 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. 防刷联运（前端配合项）

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（前端不应让用户能点）
* \[ ] 倒计时归零后按钮可点击（避免永久禁用）
