PRG模式是防重复提交的根本解法,即POST后立即303重定向至结果页,使刷新仅触发GET请求;前端禁用按钮等仅为辅助手段,无法解决刷新、后退等场景下的重复提交问题。

POST-Redirect-GET(PRG)是唯一靠谱的防重复提交方案
前端禁用按钮、加 loading、设 disabled 属性,全是障眼法。用户刷新页面、后退重进、F5 重发,照样触发第二次请求。真正有效的解法只有一个:c.Redirect() 配合 PRG 模式 —— 提交成功后立刻 303 跳转到结果页,而不是直接返回 HTML。
这么做,浏览器地址栏变成 GET 请求路径,用户再怎么刷新都不会重复提交 POST。
- 必须在处理完业务逻辑(如写数据库)且无错误后才调
c.Redirect(http.StatusSeeOther, "/success") - 跳转目标页(如
/success)应只渲染结果,不执行任何写操作 - 别用
http.StatusOK或http.StatusMovedPermanently,http.StatusSeeOther(303)语义最准确:明确告诉浏览器“用 GET 去访问新地址” - 如果需要传递提示信息(如“留言成功”),用
c.SetCookie()写临时flashcookie,跳转后读取并立即删除
为什么 Gin 里没有“防抖中间件”
搜“Gin 防抖”会看到一堆误导性文章,甚至有人封装了个 DebounceMiddleware —— 这玩意儿根本没用。服务端收不到“用户还没点下一次”,所有 HTTP 请求都是独立到达的原子事件。
你不能让 c.Next() 等 300ms 再执行,也不能把两个 POST 合成一个。所谓“服务端防抖”,实际就是限流 + 拒绝,不是延迟执行。
- 高频点击提交按钮,本质是短时间大量请求,属于接口滥用场景,该上
rate.Limiter或 Redis 限流 - 若真想控制“同一用户 5 秒内只能提交一次”,得用
sync.Map缓存user_id → *rate.Limiter,并配过期清理;多实例部署必须切到 Redis + Lua - 别把限流和防重复混为一谈:限流防刷,PRG 防误操作,两者解决的是不同层面的问题
CSRF Token 是防跨站重复提交的必要防线
即使做了 PRG,攻击者仍可能诱导用户点击恶意链接或自动提交表单。这时候没 CSRF Token,攻击就成立。
Gin 本身不内置 CSRF 支持,但实现很简单:生成随机字符串存入 Cookie,要求前端在请求头(如 X-CSRF-Token)或表单字段里带上相同值,中间件比对即可。
- Token 必须每次请求都刷新(或至少每次登录后重置),不能长期复用
- 设置 Cookie 时务必加
HttpOnly=false(否则 JS 读不到),但必须配SameSite=Lax和Secure(HTTPS 环境下) - 校验失败统一返回
http.StatusForbidden(403),不要暴露 Token 是否存在、是否过期等细节 - 注意:CSRF Token 不解决用户自己手抖连点,它只防第三方诱导提交
文件上传场景下的重复提交更隐蔽
用户选好文件点提交,前端没反馈,ta 就再点一次 —— 结果后端收到两个完全相同的 multipart/form-data 请求,都带文件。这时 c.FormFile("file") 可能成功两次,造成重复入库、重复保存文件。
关键不是“怎么拦住第二次”,而是“怎么让第二次执行时发现已存在并跳过”。靠业务层幂等性,不是靠框架拦截。
- 给每个上传请求生成唯一
request_id(前端生成 UUID 并传入 header 或 form 字段),后端存入 Redis,有效期略长于业务处理时间 - 处理前先
SETNX校验,成功才继续;失败则直接返回 “请求已在处理中” - 文件落地前,用文件内容 SHA256 做 key 查重,避免相同内容反复存储
c.ParseMultipartForm(32 的内存限制要设合理,太小导致解析失败,太大易被构造大文件耗尽内存


















