feat(auth): 短信登录防刷 — 同一设备id同IP 每小时限 5 次 (#78)
Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #78 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #78.
This commit is contained in:
@@ -74,6 +74,73 @@ 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_send_device_ip_rate_limit(client, monkeypatch) -> None:
|
||||
"""发码防刷:同一设备(device_id) + 同一 IP 每小时最多 N 次发码,超出 429。
|
||||
用不同手机号(绕开单号 60s 冷却)证明限流按设备封顶 —— 堵「换号绕开单号冷却/每日上限」的洞。
|
||||
conftest 默认关限流;本用例临时打开 + 调小阈值(避开同 IP 10/分钟那道)+ 清计数隔离。"""
|
||||
from app.api.v1 import auth
|
||||
from app.core import ratelimit
|
||||
|
||||
monkeypatch.setattr(ratelimit.settings, "RATE_LIMIT_ENABLED", True)
|
||||
monkeypatch.setattr(auth, "SMS_SEND_MAX_PER_HOUR_PER_DEVICE", 3)
|
||||
ratelimit._buckets.clear()
|
||||
|
||||
device = "dev-send-A"
|
||||
# 同设备、每次换不同手机号(绕开单号冷却)发码,前 3 次 200
|
||||
for i in range(3):
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/send",
|
||||
json={"phone": f"137001370{i:02d}", "device_id": device},
|
||||
)
|
||||
assert r.status_code == 200, f"第 {i + 1} 次应放行: {r.text}"
|
||||
# 第 4 次:同设备超限 → 429(即便又换了手机号)
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/send",
|
||||
json={"phone": "13700137003", "device_id": device},
|
||||
)
|
||||
assert r.status_code == 429, r.text
|
||||
|
||||
# 换个设备 → 独立计数(限流键含 device_id),放行
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/send",
|
||||
json={"phone": "13700137004", "device_id": "dev-send-B"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
|
||||
def test_sms_login_device_ip_rate_limit(client, monkeypatch) -> None:
|
||||
"""防刷:同一设备(device_id) + 同一 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() # 隔离:清掉跨用例累加的内存计数
|
||||
|
||||
device = "dev-rl-A"
|
||||
# 前 N 次故意每次换手机号、固定设备:mock 校验放行 → 200。证明限流按设备而非手机号,同设备共用一个桶
|
||||
for i in range(auth.SMS_LOGIN_MAX_PER_HOUR):
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": f"135331533{i:02d}", "code": "123456", "device_id": device},
|
||||
)
|
||||
assert r.status_code == 200, f"第 {i + 1} 次应放行: {r.text}"
|
||||
# 第 N+1 次:同设备同 IP 超限 → 429(即便又换了个手机号)
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": "13533153399", "code": "123456", "device_id": device},
|
||||
)
|
||||
assert r.status_code == 429, r.text
|
||||
assert "频繁" in r.json()["detail"]
|
||||
|
||||
# 同 IP 换个设备 → 独立计数(限流键含 device_id、非纯 IP/手机号),仍放行
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": "13533153300", "code": "123456", "device_id": "dev-rl-B"},
|
||||
)
|
||||
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