本文目录导读:

防止重复提交是Web开发中非常常见的需求,我来分享几种实用的Python防重提交方案。
使用Token机制(最常用)
后端实现(Flask/Django)
# forms.py
import uuid
from flask import session, jsonify, request
from functools import wraps
# 生成Token
def generate_form_token():
"""生成防重提交Token"""
token = uuid.uuid4().hex
if 'form_tokens' not in session:
session['form_tokens'] = []
session['form_tokens'].append(token)
# 限制Token数量,防止session过大
if len(session['form_tokens']) > 10:
session['form_tokens'] = session['form_tokens'][-10:]
return token
# 验证Token装饰器
def validate_token(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
token = request.form.get('_token') or request.json.get('_token')
if not token:
return jsonify({'error': '缺少安全Token'}), 400
if token not in session.get('form_tokens', []):
return jsonify({'error': '表单已提交或Token已过期'}), 400
# 移除已使用的Token
session['form_tokens'].remove(token)
return view_func(*args, **kwargs)
return wrapper
# 使用示例
@app.route('/form_page')
def form_page():
token = generate_form_token()
return render_template('form.html', token=token)
@app.route('/submit', methods=['POST'])
@validate_token
def submit_form():
# 处理表单逻辑
return jsonify({'message': '提交成功'})
前端实现
<!-- form.html -->
<form id="myForm" action="/submit" method="POST">
<input type="hidden" name="_token" value="{{ token }}">
<input type="text" name="username" required>
<button type="submit" id="submitBtn">提交</button>
</form>
<script>
document.getElementById('myForm').addEventListener('submit', function(e) {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true; // 按钮置灰
submitBtn.textContent = '提交中...';
// 还可以用一个标志位防止连续点击
});
</script>
Redis分布式锁方案(适合分布式系统)
# redis_lock.py
import redis
import time
import uuid
from functools import wraps
class RedisLock:
"""基于Redis的分布式锁"""
def __init__(self, redis_client, lock_key, expire=5):
self.redis = redis_client
self.lock_key = f"lock:{lock_key}"
self.expire = expire
self.owner = str(uuid.uuid4())
def acquire(self, timeout=0):
"""获取锁"""
end = time.time() + timeout
while timeout == 0 or time.time() < end:
# SETNX + EXPIRE 原子操作
acquired = self.redis.setnx(self.lock_key, self.owner)
if acquired:
self.redis.expire(self.lock_key, self.expire)
return True
if timeout == 0:
return False
time.sleep(0.1)
return False
def release(self):
"""释放锁 - 只释放自己持有的锁"""
# Lua脚本保证原子性
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(script, 1, self.lock_key, self.owner)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
# 使用Redis锁防重提交
class PreventDuplicateSubmit:
def __init__(self, redis_host='localhost', redis_port=6379):
self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)
def prevent_duplicate(self, key_func=None, expire=5):
"""装饰器,防止重复提交"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 生成唯一锁键
if key_func:
lock_key = key_func(*args, **kwargs)
else:
# 默认使用请求参数生成键
request = kwargs.get('request') or args[0]
lock_key = f"{func.__name__}:{request.remote_addr}:{request.form.get('id', '')}"
lock = RedisLock(self.redis, lock_key, expire=expire)
if lock.acquire(timeout=0):
try:
return func(*args, **kwargs)
finally:
lock.release()
else:
return {"error": "操作正在进行中,请勿重复提交"}, 429
return wrapper
return decorator
# 使用示例
prevent = PreventDuplicateSubmit()
@app.route('/order/create', methods=['POST'])
@prevent.prevent_duplicate(key_func=lambda req: f"order:{req.form.get('product_id')}:{req.remote_addr}")
def create_order():
# 创建订单逻辑
pass
使用数据库唯一约束
# model_unique.py
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, UniqueConstraint
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import IntegrityError
Base = declarative_base()
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
order_no = Column(String(64), unique=True) # 唯一约束
user_id = Column(Integer)
product_id = Column(Integer)
amount = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
UniqueConstraint('user_id', 'product_id', name='user_product_uk'),
)
# 防重提交服务
class PreventDuplicateService:
def __init__(self, session_factory):
self.Session = session_factory
@staticmethod
def generate_order_no(user_id, product_id):
"""生成业务唯一标识"""
timestamp = int(time.time() * 1000)
return f"ORD{user_id}{product_id}{timestamp}"
def create_order(self, user_id, product_id, amount):
"""幂等创建订单"""
session = self.Session()
try:
# 先检查是否存在
existing = session.query(Order).filter_by(
user_id=user_id,
product_id=product_id
).first()
if existing:
return {
'success': False,
'message': '该商品已下单',
'data': existing
}
# 创建订单
order = Order(
order_no=self.generate_order_no(user_id, product_id),
user_id=user_id,
product_id=product_id,
amount=amount
)
session.add(order)
session.commit()
return {
'success': True,
'message': '订单创建成功',
'data': order
}
except IntegrityError:
session.rollback()
return {
'success': False,
'message': '订单已存在,请勿重复提交'
}
finally:
session.close()
前端+后端综合方案(最推荐)
# comprehensive_prevent.py
import hashlib
import json
import time
from functools import wraps
class ComprehensiveAntiDuplicate:
"""综合防重提交方案"""
def __init__(self, cache_client):
self.cache = cache_client # 可以是Redis, Memcached等
def _generate_request_fingerprint(self, request):
"""生成请求指纹"""
data = {
'url': request.path,
'method': request.method,
'body': request.get_data(as_text=True),
'timestamp': int(time.time() // 2), # 2秒内的请求视为重复
'user': request.remote_addr
}
return hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()
def anti_duplicate(self, expire=3):
"""防重提交装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
request = kwargs.get('request') or args[0]
# 1. 前端防重(检查Token)
client_token = request.headers.get('X-Request-Id') or request.form.get('_request_id')
if not client_token:
return {'error': '缺少请求标识'}, 400
# 2. 后端防重(检查请求指纹)
fingerprint = self._generate_request_fingerprint(request)
cache_key = f"duplicate:{fingerprint}"
if self.cache.get(cache_key):
# 检查是否是幂等请求(允许重试)
if request.method in ['GET', 'HEAD', 'OPTIONS']:
pass # 幂等方法允许重复
else:
return {'error': '请求正在处理或已处理,请勿重复提交'}, 429
# 3. 设置缓存标记
self.cache.setex(cache_key, expire, client_token)
try:
return func(*args, **kwargs)
except Exception as e:
# 请求失败时清理缓存
self.cache.delete(cache_key)
raise e
return wrapper
return decorator
# 5. 前端完整防重提交JavaScript
// anti_duplicate.js
class FormAntiDuplicate {
constructor() {
this.submitted = new Set();
this.submitLock = false;
}
// 生成唯一请求ID
generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 添加请求到已提交列表
addSubmitted(formId) {
this.submitted.add(formId);
}
// 检查是否已提交
isSubmitted(formId) {
return this.submitted.has(formId);
}
// 移除请求
removeSubmitted(formId) {
this.submitted.delete(formId);
}
// 防重提交的表单提交方法
async submitForm(formElement, submitUrl) {
const formId = formElement.id || formElement.name;
// 1. 检查是否已提交
if (this.isSubmitted(formId)) {
console.error('表单已提交,请勿重复操作');
return false;
}
// 2. 检查是否正在提交
if (this.submitLock) {
console.error('正在提交中,请稍候');
return false;
}
// 3. 设置锁定状态
this.submitLock = true;
this.addSubmitted(formId);
try {
// 4. 添加请求标识
const requestId = this.generateRequestId();
const formData = new FormData(formElement);
formData.append('_request_id', requestId);
// 5. 发送请求
const response = await fetch(submitUrl, {
method: 'POST',
body: formData,
headers: {
'X-Request-Id': requestId
}
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '提交失败');
}
// 6. 提交成功
this.removeSubmitted(formId);
return result;
} catch (error) {
// 7. 提交失败
this.removeSubmitted(formId);
throw error;
} finally {
// 8. 释放锁定
this.submitLock = false;
}
}
}
// 使用示例
const antiDuplicate = new FormAntiDuplicate();
document.getElementById('submitBtn').addEventListener('click', async function(e) {
e.preventDefault();
const form = document.getElementById('myForm');
const submitBtn = this;
try {
// 禁用按钮
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
// 防重提交
const result = await antiDuplicate.submitForm(form, '/api/submit');
if (result) {
alert('提交成功!');
}
} catch (error) {
alert(error.message);
} finally {
// 恢复按钮
submitBtn.disabled = false;
submitBtn.textContent = '提交';
}
});
最佳实践建议
- 多层防御:前端+后端+数据库三层防重
- 幂等设计:接口设计为幂等的,允许重复请求
- 合理超时:设置合理的锁超时时间(通常3-5秒)
- 异常处理:处理好网络超时、服务异常等情况
- 日志记录:记录重复提交日志便于排查
- 用户提示:友好的用户提示,不要直接显示技术错误
选择哪种方案取决于你的具体需求:
- 单机应用:Token机制就够了
- 分布式系统:Redis分布式锁
- 高并发场景:数据库唯一约束+幂等设计
- 最佳实践:综合使用多个方案