feat: 添加幂等性中间件 防止重复提交
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
18
backend/middleware/IdempotencyMiddleware.py
Normal file
18
backend/middleware/IdempotencyMiddleware.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
27
backend/utils/decorators.py
Normal file
27
backend/utils/decorators.py
Normal 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
|
||||
32
backend/utils/idempotency_helper.py
Normal file
32
backend/utils/idempotency_helper.py
Normal 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"
|
||||
Reference in New Issue
Block a user