feat: 添加幂等性中间件 防止重复提交

This commit is contained in:
XIE7654
2025-10-16 20:21:55 +08:00
parent 42d5a3da21
commit 1756f549c9
6 changed files with 90 additions and 10 deletions

View File

@@ -67,11 +67,12 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'middleware.IdempotencyMiddleware.IdempotencyMiddleware', # 幂等性中间件 防止重复提交
]
# 演示环境中间件 - 全局禁止修改和删除操作
if DEMO_MODE:
MIDDLEWARE.append('utils.middleware.DemoModeMiddleware')
MIDDLEWARE.append('middleware.DemoModeMiddleware.DemoModeMiddleware')
AUTH_USER_MODEL = 'system.User'
ROOT_URLCONF = 'backend.urls'

View File

@@ -9,35 +9,35 @@ class DemoModeMiddleware(MiddlewareMixin):
演示环境中间件
全局禁止修改和删除操作
"""
def process_request(self, request):
# 只处理 API 请求
if not request.path.startswith('/api/'):
return None
# 禁止的 HTTP 方法
forbidden_methods = ['POST', 'PUT', 'PATCH', 'DELETE']
if request.method in forbidden_methods:
# 检查是否是登录接口,登录接口允许 POST
if request.path.endswith('/login/') or request.path.endswith('/auth/login/'):
return None
# 检查是否是登出接口,登出接口允许 POST
if request.path.endswith('/logout/') or request.path.endswith('/auth/logout/'):
return None
# 其他修改/删除操作一律禁止
response_data = {
'code': 403,
'message': '演示环境禁止修改和删除操作',
'data': None
}
return JsonResponse(
response_data,
response_data,
status=status.HTTP_403_FORBIDDEN,
content_type='application/json'
)
return None
return None

View File

@@ -0,0 +1,18 @@
from django.http import JsonResponse
from utils.idempotency_helper import generate_idempotency_key, check_idempotency, get_user_identifier
class IdempotencyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.method == "POST":
user_id = get_user_identifier(request)
key = generate_idempotency_key(user_id, request.path, request.body)
if check_idempotency(key, 10):
return JsonResponse({"error": "请勿重复提交"}, status=409)
response = self.get_response(request)
return response

View File

@@ -1,6 +1,7 @@
from rest_framework import viewsets, status
from rest_framework.response import Response
# from utils.decorators import idempotent
from utils.export_mixin import ExportMixin
@@ -102,6 +103,7 @@ class CustomModelViewSet(viewsets.ModelViewSet, ExportMixin):
status=status.HTTP_200_OK
)
# @idempotent(timeout=10) # 幂等性装饰器,防止重复提交
def create(self, request, *args, **kwargs):
"""重写创建视图,支持批量创建"""
is_many = isinstance(request.data, list)

View File

@@ -0,0 +1,27 @@
from rest_framework.response import Response
from rest_framework import status
from utils.idempotency_helper import generate_idempotency_key, check_idempotency, get_user_identifier
def idempotent(timeout=10):
"""
幂等性装饰器用于单个DRF接口
:param timeout: 重复判断时间窗口(秒)
"""
def decorator(view_func):
def wrapper(self, request, *args, **kwargs):
user_id = get_user_identifier(request)
key = generate_idempotency_key(user_id, request.path, request.body)
if check_idempotency(key, timeout):
return Response(
{"error": "请勿重复提交"},
status=status.HTTP_409_CONFLICT
)
return view_func(self, request, *args, **kwargs)
return wrapper
return decorator

View File

@@ -0,0 +1,32 @@
import hashlib
from django.core.cache import cache
def generate_idempotency_key(user_id, path, body):
"""
生成幂等性检查的唯一标识key
:param user_id: 用户ID或"anonymous"
:param path: 请求路径
:param body: 请求体内容
:return: MD5哈希值作为唯一标识
"""
return hashlib.md5(f"{user_id}_{path}_{body}".encode()).hexdigest()
def check_idempotency(key, timeout=10):
"""
检查是否为重复请求
:param key: 幂等性标识key
:param timeout: 缓存超时时间(秒)
:return: True表示重复请求False表示首次请求
"""
if cache.get(key):
return True
cache.set(key, "processing", timeout)
return False
def get_user_identifier(request):
"""
获取用户标识符
:param request: HTTP请求对象
:return: 用户ID或"anonymous"
"""
return request.user.id if request.user.is_authenticated else "anonymous"