fix: auth 畸形body防御/字段校验信封/用户名校验/429 测试

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 14:18:37 +08:00
parent 3e58fe003f
commit 7ff1536fbf
2 changed files with 60 additions and 8 deletions
+19 -8
View File
@@ -1,3 +1,4 @@
import re
from django.contrib.auth import get_user_model
from rest_framework.views import APIView
from rest_framework.response import Response
@@ -5,6 +6,9 @@ from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.throttling import ScopedRateThrottle
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
# 对齐 Django UnicodeUsernameValidator(\w 默认 Unicode)——create_user 不跑校验器,入口自防
_USERNAME_RE = re.compile(r"[\w.@+-]+")
def _ok(data, message="ok"):
return Response({"code": 200, "data": data, "message": message})
@@ -20,9 +24,13 @@ class _EnvelopeMixin:
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
if response.status_code >= 400 and isinstance(response.data, dict) and "detail" in response.data:
response.data = {"code": response.status_code, "data": None,
"message": response.data["detail"]}
if response.status_code >= 400 and isinstance(response.data, dict):
if "detail" in response.data:
response.data = {"code": response.status_code, "data": None,
"message": response.data["detail"]}
elif "code" not in response.data: # simplejwt 序列化器字段错误 {"password": [...]} 等
response.data = {"code": response.status_code, "data": response.data,
"message": "invalid_input"}
return response
@@ -59,11 +67,14 @@ class UserCreateView(_EnvelopeMixin, APIView):
permission_classes = [IsAdminUser] # is_staff
def post(self, request):
username = (request.data.get("username") or "").strip()
password = request.data.get("password") or ""
# 用户名长度上限对齐 Django User.username(max_length=150)——create_user 不跑
# full_clean,超长会直奔 MySQL DataError 500,故入口拦截
if not username or len(username) > 150 or len(password) < 8:
data = request.data if isinstance(request.data, dict) else {} # JSON 数组等畸形 body → 空表(.get 会 500)
username, password = data.get("username"), data.get("password")
if not isinstance(username, str) or not isinstance(password, str): # 123.strip() / len(None) 会 500
return Response({"code": 400, "data": None, "message": "invalid_input"}, status=400)
username = username.strip()
# 字符集对齐 UnicodeUsernameValidator(create_user 不跑校验器);长度上限防 MySQL DataError(sqlite 不报错)
if (not username or len(username) > 150 or not _USERNAME_RE.fullmatch(username)
or len(password) < 8):
return Response({"code": 400, "data": None, "message": "invalid_input"}, status=400)
User = get_user_model()
if User.objects.filter(username=username).exists():
+41
View File
@@ -85,3 +85,44 @@ def test_user_create_validates_input(client, admin_user):
{"username": "x", "password": "short"}, format="json").status_code == 400
assert client.post("/api/admin/users",
{"username": "admin", "password": "longenough1"}, format="json").status_code == 409
# ---- 审查修复锁定测试 ----
def test_user_create_malformed_body_400_not_500(client, admin_user):
"""探针证实:JSON 数组 body / 非字符串 username/password 直接 500——入口统一 400 invalid_input。"""
_auth(client, admin_user)
assert client.post("/api/admin/users", ["arr"], format="json").status_code == 400
assert client.post("/api/admin/users",
{"username": 123, "password": "longenough1"}, format="json").status_code == 400
assert client.post("/api/admin/users",
{"username": "okname", "password": 12345678}, format="json").status_code == 400
def test_login_missing_password_field_envelope(client, admin_user):
"""simplejwt 缺 password 的 400 是 {"password":[...]} 字段校验形状——信封统一,前端单条解析路径。"""
r = client.post("/api/admin/auth/token", {"username": "admin"}, format="json")
assert r.status_code == 400
body = r.json()
assert body["code"] == 400 and body["message"] == "invalid_input" and body["data"]
def test_user_create_username_charset_and_length(client, admin_user):
"""字符集对齐 UnicodeUsernameValidator(create_user 不跑校验器);>150 防 MySQL DataError(sqlite 不报错须入口拦)。"""
_auth(client, admin_user)
assert client.post("/api/admin/users",
{"username": "bad name", "password": "longenough1"}, format="json").status_code == 400
assert client.post("/api/admin/users",
{"username": "x" * 151, "password": "longenough1"}, format="json").status_code == 400
assert client.post("/api/admin/users",
{"username": "ok.name+1", "password": "longenough1"}, format="json").status_code == 200
def test_login_throttle_429_envelope(client, admin_user):
"""直接打默认 10/min(THROTTLE_RATES import 时固化为类属性,override_settings 提速无效,
同 collect.py CollectThrottle.get_rate 注释)——第 11 次 429 且带信封。"""
for _ in range(10):
assert client.post("/api/admin/auth/token",
{"username": "admin", "password": "wrongpass"}, format="json").status_code == 401
r = client.post("/api/admin/auth/token",
{"username": "admin", "password": "wrongpass"}, format="json")
assert r.status_code == 429
body = r.json()
assert body["code"] == 429 and body["data"] is None and body["message"]