From 7ff1536fbf1ae35f3f93aaaf903f354e303a9032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 14:18:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20auth=20=E7=95=B8=E5=BD=A2body=E9=98=B2?= =?UTF-8?q?=E5=BE=A1/=E5=AD=97=E6=AE=B5=E6=A0=A1=E9=AA=8C=E4=BF=A1?= =?UTF-8?q?=E5=B0=81/=E7=94=A8=E6=88=B7=E5=90=8D=E6=A0=A1=E9=AA=8C/429=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- server/tracker/api/auth.py | 27 ++++++++++++++------ server/tracker/tests/test_auth.py | 41 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/server/tracker/api/auth.py b/server/tracker/api/auth.py index e242dda..2d3786d 100644 --- a/server/tracker/api/auth.py +++ b/server/tracker/api/auth.py @@ -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(): diff --git a/server/tracker/tests/test_auth.py b/server/tracker/tests/test_auth.py index 32e98a6..baf4c70 100644 --- a/server/tracker/tests/test_auth.py +++ b/server/tracker/tests/test_auth.py @@ -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"]