feat(auth): 短信登录防刷 — 同手机号同IP 每小时限 5 次
/sms/login 增加 (手机号+IP) 每小时最多 5 次限流,挡撞库爆破/高频重登; 与路由上原有 IP 维度限流(20次/分钟,跨号)互补。 - 计数放在验证码校验之前 → 输错码的失败尝试也计数 - 测试账号(.env TEST_ACCOUNT_PHONE)豁免,走自己的每日额度 - 复用 ratelimit 内存固定窗口,受 RATE_LIMIT_ENABLED 总开关控制(默认开) - 新增 enforce_rate_limit(供路由内按请求体字段如手机号限流) - 补 test_auth/test_test_account 回归测试(普通号到5次被拦、测试号豁免) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,30 @@ def test_sms_send_too_frequent(client) -> None:
|
||||
assert client.post("/api/v1/auth/sms/send", json={"phone": phone}).status_code == 429
|
||||
|
||||
|
||||
def test_sms_login_phone_ip_rate_limit(client, monkeypatch) -> None:
|
||||
"""防刷:同一手机号 + 同一 IP 每小时最多 SMS_LOGIN_MAX_PER_HOUR 次登录尝试,超出 429。
|
||||
conftest 默认 RATE_LIMIT_ENABLED=false(内存计数跨用例累加),本用例临时打开并清空计数隔离。"""
|
||||
from app.api.v1 import auth
|
||||
from app.core import ratelimit
|
||||
|
||||
monkeypatch.setattr(ratelimit.settings, "RATE_LIMIT_ENABLED", True)
|
||||
ratelimit._buckets.clear() # 隔离:清掉跨用例累加的内存计数
|
||||
|
||||
phone = "13533153300"
|
||||
# 前 N 次:mock 校验放行(任意 6 位数字)→ 200 登录成功
|
||||
for i in range(auth.SMS_LOGIN_MAX_PER_HOUR):
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
|
||||
assert r.status_code == 200, f"第 {i + 1} 次应放行: {r.text}"
|
||||
# 第 N+1 次:同号同 IP 超限 → 429
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
|
||||
assert r.status_code == 429, r.text
|
||||
assert "频繁" in r.json()["detail"]
|
||||
|
||||
# 同 IP 换个手机号 → 独立计数(限流键含手机号、非纯 IP),仍放行
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": "13533153301", "code": "123456"})
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
|
||||
def test_sms_login_bad_code(client) -> None:
|
||||
"""mock 下校验位数:5 位过 schema 但业务要 6 位 → 400。"""
|
||||
phone = "13700137000"
|
||||
|
||||
Reference in New Issue
Block a user