html表单提交时token必须由后端动态注入到中,浏览器才会在post时自动携带;其他方式如meta、cookie、js变量等均无效,ajax需手动提取并传入。

HTML表单提交时携带 Token,本质不是“前端主动传”,而是后端在渲染页面时把 Token 塞进表单字段,再由浏览器原生 POST 机制自动带上——不手动操作就根本带不走。
Token 必须作为 <input type="hidden"> 嵌入表单内部
浏览器只会在 <form method="post"></form> 提交时,把表单内所有 <input>、<select></select>、<textarea></textarea> 的 name/value 对按 application/x-www-form-urlencoded 编码发出去。其他地方放的 Token 都无效:
-
<meta name="csrf-token" content="xxx">→ 浏览器不会自动提交 -
document.cookie = "token=xxx"→ 表单 POST 不会把 cookie 当 body 字段发 <div data-token="xxx"> → 完全不参与表单序列化 <li>JS 变量 <code>const token = "xxx"→ 更不可能随 form.submit() 发出去- 模板里硬编码
value="static_token_2026"→ 所有用户共享同一 Token,形同虚设 - 前端用
Math.random().toString(36)生成 → 攻击者可预测、可重放 - 从
localStorage.getItem('token')读取后塞进 hidden 字段 → 这是 JWT,不是 CSRF Token;二者用途不同,混用会导致防护失效
正确写法只有一种:<input type="hidden" name="csrf_token" value="abc123def456">,且该标签必须是 <form></form> 的直接子元素或后代元素(能被 form.elements 访问到)。
后端注入值不能写死,也不能由前端生成
Token 的安全性取决于它是否“一次一密”且绑定当前会话。常见错误包括:
必须由后端在响应 HTML 时动态生成并注入,例如 Flask 模板:<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">,其值来自 session 或短期有效的服务端缓存。
Ajax 提交时 Token 不会自动附带,必须手动提取
如果你用 fetch() 或 XMLHttpRequest 提交表单(比如阻止默认行为后异步发包),浏览器不会自动读取 hidden 字段内容。此时需显式获取:
const tokenInput = document.querySelector('input[name="csrf_token"]');
const token = tokenInput ? tokenInput.value : '';
fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
username: 'a',
password: 'b',
csrf_token: token // 必须手动加进去
})
});
注意:如果后端同时支持传统表单 POST 和 Ajax,Token 的存储位置(如 session key 名)、过期逻辑、比对方式必须完全一致,否则会出现“点按钮能过,调 API 就 403”的问题。
验证失败时别暴露 Token 状态细节
后端校验 csrf_token 字段失败,统一返回 403 Forbidden 即可,不要区分“Token 不存在”“Token 已过期”“Token 格式错误”。原因有二:
- 泄露信息可能被用于探测会话状态或爆破策略
- 前端无法据此做有意义的重试——Token 是一次性且绑定会话的,错就是错了,必须刷新页面重取
真正容易被忽略的是 Token 生命周期与页面停留时间的错配:用户打开登录页后闲置 30 分钟再提交,Token 很可能已被后端清除。这种场景下,与其前端加倒计时提示,不如后端返回 403 后前端自动 reload 页面——简单、可靠、不增加复杂度。











