诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513

This commit is contained in:
seagull
2026-09-12 02:20:50 +08:00
parent b3f3095d53
commit 67540e8d30
40 changed files with 1805 additions and 31 deletions
+93
View File
@@ -0,0 +1,93 @@
"""P0-5 · 登录防爆破 + API Key 限流回归。
- 主登录 `/api/v1/auth/token/`:5 次失败后锁定 15 分钟(429),正确密码也被锁;
成功登录清零(正常用户不受影响)。
- 商城登录 `/api/v1/storefront/login/`:同策略(按 phone + IP)。
- API Key:分钟窗口超 `rate_limit` → 429;`rate_limit<=0` 下界为 1。
"""
import pytest
from django.core.cache import cache
from rest_framework.test import APIClient
from apps.core import ratelimit as rl
from apps.openapi.models import APIKey
@pytest.fixture(autouse=True)
def _clean_cache():
cache.clear()
yield
cache.clear()
def test_main_login_locks_after_5_failures(db, django_user_model):
django_user_model.objects.create_user("victim", password="correct-pw")
c = APIClient()
for _ in range(4):
r = c.post("/api/v1/auth/token/",
{"username": "victim", "password": "wrong"},
format="json")
assert r.status_code == 401, r.content
# 第 5 次失败达阈值直接锁定(429),后续正确密码也被锁
fifth = c.post("/api/v1/auth/token/",
{"username": "victim", "password": "wrong"},
format="json")
assert fifth.status_code == 429, fifth.content
assert fifth.json()["code"] == "login_locked"
locked = c.post("/api/v1/auth/token/",
{"username": "victim", "password": "wrong"},
format="json")
assert locked.status_code == 429, locked.content
assert locked.json()["code"] == "login_locked"
# 正确密码也被锁
good = c.post("/api/v1/auth/token/",
{"username": "victim", "password": "correct-pw"},
format="json")
assert good.status_code == 429, good.content
def test_main_login_success_resets_counter(db, django_user_model):
django_user_model.objects.create_user("normal", password="pw123456")
c = APIClient()
for _ in range(4):
c.post("/api/v1/auth/token/",
{"username": "normal", "password": "wrong"}, format="json")
ok = c.post("/api/v1/auth/token/",
{"username": "normal", "password": "pw123456"}, format="json")
assert ok.status_code == 200, ok.content
# 计数已清:再错 4 次仍是 401 而非 429
for _ in range(4):
r = c.post("/api/v1/auth/token/",
{"username": "normal", "password": "wrong"}, format="json")
assert r.status_code == 401, r.content
def test_apikey_rate_limit_enforced(db, tenant):
from django.contrib.auth import get_user_model
User = get_user_model()
u = User.objects.create_user("keyboss", password="x")
key_obj, raw = APIKey.generate(
tenant=tenant, name="limited", scopes=["products:read"], created_by=u,
)
key_obj.rate_limit = 3
key_obj.save(update_fields=["rate_limit"])
c = APIClient()
c.credentials(HTTP_X_API_KEY=raw)
for _ in range(3):
r = c.get("/api/v1/openapi/v1/products/")
assert r.status_code == 200, r.content
over = c.get("/api/v1/openapi/v1/products/")
assert over.status_code == 429, over.content
# DRF 只在 exc.wait 非空时才回 Retry-After;不传 wait 只有 429 无头。
assert "Retry-After" in over, dict(over.items())
def test_effective_rate_limit_floor():
assert rl.effective_rate_limit(0) == 1
assert rl.effective_rate_limit(-5) == 1
assert rl.effective_rate_limit(None) == 1
assert rl.effective_rate_limit("abc") == 1
assert rl.effective_rate_limit(120) == 120