重复提交会导致数据异常,因django默认不拦截多次post请求,造成订单、评论、积分等重复创建;需结合csrf token轮换与submit_id幂等校验实现服务端兜底防护。

为什么重复提交会导致数据异常
用户快速点击两次“提交”按钮,或者刷新已提交成功的页面再点后退+刷新,都可能触发多次 POST 请求。Django 默认不会拦截重复请求,后端照常执行视图逻辑——结果就是一条订单被创建两次、评论被发两遍、积分被加了两回。
这不是前端防抖能彻底解决的问题:禁用按钮可被绕过(比如禁用前就双击),localStorage 标记在多标签页下失效,且服务端仍需兜底。
用 Django 的 django.views.decorators.csrf.csrf_protect + 一次性 Token
Django 自带的 CSRF 机制其实已经提供了防重能力的基础:每个表单渲染时会生成一个 csrf_token,而该 token 在验证通过后会被轮换(取决于 CSRF_USE_SESSIONS 设置)。但默认不阻止重复提交,需要配合手动控制 token 生命周期。
- 在视图中处理成功后,立即调用
rotate_token(request)—— 这会让下次同 session 的相同 token 失效 - 模板中必须使用
{% csrf_token %},不能手写隐藏域或跳过 CSRF - 确保
settings.py中CSRF_USE_SESSIONS = True(推荐),否则 token 存在 Cookie 中,重复提交可能仍被接受
示例片段:
from django.views.decorators.csrf import csrf_protect
from django.utils.crypto import rotate_token
<p>@csrf_protect
def order_create(request):
if request.method == 'POST':
form = OrderForm(request.POST)
if form.is_valid():
form.save()
rotate_token(request) # 关键:让本次 token 失效
return redirect('success')
else:
form = OrderForm()
return render(request, 'order_form.html', {'form': form})
</p>
更可靠的做法:服务端生成并校验唯一提交 ID(submit_id)
CSRF token 轮换依赖 session 状态,而某些场景(如无登录用户、跨域 iframe 提交)下 session 不稳定。此时应引入业务层的幂等控制。
- 在 GET 表单页时,生成一个随机
submit_id(如uuid.uuid4().hex),存入 session 或缓存(如 Redis),同时作为隐藏字段写入表单:<input type="hidden" name="submit_id" value="{{ submit_id }}"> - POST 视图中先检查
request.POST.get('submit_id')是否存在且未被标记为“已处理”;若已存在,直接返回HttpResponseBadRequest或重定向到成功页 - 校验通过后,立刻将该
submit_id写入缓存并设短过期(如 5 分钟),再执行业务逻辑 - 注意:不要仅依赖数据库唯一约束做兜底——它只能防止写入失败,不能避免中间状态(如信号触发、异步任务派发)被重复执行
前端配合不能少,但别信它能单独解决问题
JavaScript 层面禁用提交按钮是最基本操作,但它只是用户体验优化,不是安全边界。
- 用
form.addEventListener('submit', e => { btn.disabled = true; }),而非只靠 CSS 类控制 - 避免在
fetch或axios成功回调里才禁用按钮——网络延迟可能导致用户多次点击 - 如果用了 SPA 框架(如 Vue/React),确保组件卸载时清除 pending 状态,防止路由返回后按钮恢复可用
- 特别注意:Safari 对
history.back()后的表单重新提交行为较激进,必须和服务端submit_id方案配合
真正起作用的是服务端对每次请求的唯一性判定,前端只是减少误触概率。只要没在服务端落地幂等逻辑,任何前端限制都形同虚设。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











