防重复提交必须靠服务端机制,前端按钮置灰或JS锁只是障眼法;核心解法是ValidateAntiForgeryToken防CSRF+一次性令牌,以及Redis实现接口级幂等(Idempotency-Key头+原子写入)。

防重复提交必须靠服务端机制,前端按钮置灰或 JS 锁只是障眼法;真实场景下,绕过页面直接发请求、Postman 调试、PDA 扫码重发、网络抖动导致超时重试——这些都会让前端控制彻底失效。核心解法就两条:一是用 ValidateAntiForgeryToken 拦 CSRF + 一次性表单令牌,二是用 Redis 实现接口级幂等(Idempotency-Key 头 + 原子写入)。
ValidateAntiForgeryToken 失效的典型表现和修复点
它不是“加个特性就完事”,而是依赖一整套配套链路。常见问题不是代码写错,而是环境或配置断在某个环节:
-
ValidateAntiForgeryTokenAttribute报 400 但没具体错误信息 → 检查是否漏了builder.Services.AddAntiforgery(),尤其在 Minimal Hosting 模式下容易被忽略 - 用户刷新页面后再次提交成功 → 说明 token 没做“一次性”校验,确认控制器方法是否同时标记了
[HttpPost]和[ValidateAntiForgeryToken],二者缺一不可 - AJAX 提交失败 → 前端没读取
__RequestVerificationToken隐藏字段,或没把它塞进请求头(必须是X-CSRF-TOKEN,X-Requested-With不认) - 部署到多台服务器后偶发失败 →
DataProtection密钥未共享,需调用services.AddDataProtection().PersistKeysToSqlServer(...)或挂载到共享目录
Redis 幂等中间件怎么写才不翻车
用 SET key value EX 3600 NX 是底线,但生产环境光原子写还不够。真实踩坑点集中在 key 设计、缓存内容、过期策略三块:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- key 必须带业务上下文前缀,比如
idemp_{userId}_{orderId}_v1,避免不同用户/订单冲突;别用纯时间戳或 GUID 当 key,缺乏语义且难排查 - 缓存里只存状态码 + 简要结果(如
{"status":"success","orderId":"ORD-20260426-001"}),不要塞完整响应体,否则内存和序列化开销飙升 - 过期时间得设成
AbsoluteExpirationRelativeToNow,不能靠滑动过期;若业务耗时波动大(比如支付回调可能卡 5 秒),过期时间至少比最大预期耗时多留 2 秒余量 - 返回 409 Conflict 时,必须把缓存里的结果原样返回,而不是抛异常或空响应——客户端靠这个判断“上次已成功”,否则会误以为失败而重试
为什么不用 IMemoryCache 做幂等
它快,但只适合单机调试或极低并发内部工具。一旦上负载均衡或容器扩缩容,IMemoryCache 就变成各节点各自为政:
- 用户 A 的请求打到 Server1,写入
idemp_abc成功;下一秒重试打到 Server2,查不到该 key,直接放行 → 数据重复 - 没内置清理机制,
ConcurrentDictionary+ 定时器手动清理容易漏掉或误删,线上出过因缓存堆积导致 GC 频繁的案例 - 无法处理跨服务调用(比如订单服务调库存服务),Redis 是唯一能被所有服务共视的协调点
真正难的不是写对一行 SET 命令,而是想清楚:这个幂等 key 的生命周期是否覆盖了所有重试路径?客户端有没有能力生成稳定、可追溯的 Idempotency-Key?缓存结果是否包含足够信息让前端或下游服务做出正确决策?这些细节漏掉一个,上线后就是半夜告警。

















