防重复提交必须用服务端令牌,前端拦截只是辅助;核心是服务端生成一次性AntiForgeryToken并校验其有效性与唯一性,部署时需统一DataProtection密钥、正确配置SameSite属性。
防重复提交必须用服务端令牌,前端拦截只是辅助
光靠按钮置灰、js加锁或时间戳判断,拦不住绕过前端的请求。核心防线在服务端生成一次性的 antiforgerytoken,每次表单提交都强制校验它是否有效且未被用过。
常见错误现象:ValidateAntiForgeryTokenAttribute 报 400 错误,但页面没报错;用户刷新页面后再次提交成功;AJAX 提交时 token 丢失导致验证失败。
- ASP.NET Core 中,
@Html.AntiForgeryToken()或会自动注入隐藏字段__RequestVerificationToken - 对应控制器方法必须标记
[HttpPost]+[ValidateAntiForgeryToken],二者缺一不可 - 若用 AJAX 提交,需从 DOM 中手动读取
__RequestVerificationToken值,并作为请求头(如X-Requested-With不行,得用X-CSRF-TOKEN)或表单字段传入 - 不要在多个表单共用同一个
ViewBag或静态变量存 token —— 每次请求必须生成新 token
ValidateAntiForgeryToken 的原理和失效场景
它本质是基于 cookie + 隐藏字段的对称加密比对:服务端把加密后的 token 存进 .AspNetCore.Cookies,同时把另一份解密所需的 key 和随机盐值塞进隐藏字段。验证时两边解密比对,任一环节被篡改或过期即失败。
容易踩的坑:
- 部署到负载均衡环境时,没统一配置
DataProtection密钥,导致不同服务器无法互相验证 token —— 必须调用services.AddDataProtection().PersistKeysToSqlServer(...)或共享文件系统 - 用户长时间不操作,cookie 过期但页面仍开着,点提交时 token 字段还在,但服务端已无法解密 —— 此时应捕获
AntiforgeryValidationException并返回友好提示,而不是 500 - 在
Program.cs中漏掉builder.Services.AddAntiforgery(),会导致整个应用无 token 生效能力
自定义防伪逻辑时别碰 IAntiforgery 的底层细节
除非你明确要替换默认实现(比如对接硬件签名设备),否则直接用框架内置机制最稳。手写 token 生成+校验极易出错,尤其在并发、时钟漂移、密钥轮换等边界下。
真实使用场景中更常遇到的是「部分接口不需要 token」或「某些客户端(如 Postman 调试)临时绕过」:
- 跳过验证用
[IgnoreAntiforgeryToken],但仅限于明确无敏感操作的 GET 接口或健康检查 - 测试环境可通过
IWebHostEnvironment.IsDevelopment()动态决定是否添加[ValidateAntiForgeryToken],但上线前务必删掉这类逻辑 - 不要试图用
IAntiforgery.GetAndStoreTokens()在中间件里给每个响应塞 token —— 它只应在 Razor 页面或 MVC View 中按需生成,否则会污染缓存、引发并发冲突
前后端分离项目怎么安全传 token
Vue/React 单页应用调用 ASP.NET Core API 时,@Html.AntiForgeryToken() 失效,因为没有服务端渲染的 form。此时必须显式管理 token 生命周期。
关键动作:
- 首次加载页面时,发一个 GET 请求到
/api/antiforgery(该接口返回{ requestToken: "xxx", cookieValue: "yyy" }) - 前端把
cookieValue写入 document.cookie(注意设置SameSite=Lax和Secure),再把requestToken存入内存或 pinia/vuex - 后续 POST/PUT 请求,将
requestToken放入请求头X-XSRF-TOKEN(注意不是X-CSRF-TOKEN,这是 Angular 默认约定,.NET Core 默认认前者) - 服务端启用
services.AddAntiforgery(o => o.HeaderName = "X-XSRF-TOKEN"),并确保前端 cookie 和 header 的 token 来自同一生成批次
这个链路里最容易被忽略的是 cookie 的 SameSite 属性 —— 如果前端域名和 API 域名不同(如 localhost:3000 → api.example.com),必须设为 None 且开启 Secure,否则浏览器根本不发 cookie,验证必然失败。










