Session绑定CSRF Token后前端怎么正确发送?

一艺诺 阅读 17

我后端用Session存了CSRF Token,登录后返回给前端,但我每次提交表单时还是被拦截。是不是我发请求的方式不对?

我试过把token放在header里,也试过放在body里,但都失败了。后端同事说必须和Session里的token一致,可我明明拿到的是同一个值啊……

const handleSubmit = async () => {
  const token = sessionStorage.getItem('csrfToken');
  await fetch('/api/update-profile', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    body: JSON.stringify({ name: 'test' })
  });
};
我来解答 赞 3 收藏
二维码
手机扫码查看
2 条解答
景鑫 Dev
你后端拦截的根本原因不是token值不对,而是你把token放在header里发,但后端其实是在session里校验的——它压根没看header里的X-CSRF-Token字段。

常见做法是:后端生成token后,把它写进session,同时返回给前端;前端提交时,要么把token放进cookie(同源自动带),要么放进表单hidden字段,或者放在body里某个固定字段(比如叫csrf_token),但关键是你得确认后端到底怎么校验的。

先让后端确认两件事:
1. 他校验CSRF时,是从哪个位置取token?是request header?还是request body?还是cookie?
2. 是不是只认session里存的那个值,不认你传上来的?

如果后端确认只认session里的token(典型是Django、Spring Security默认行为),那前端光传值没用——你得保证请求带上了正确的session cookie(比如SameSite=None; Secure的cookie),否则浏览器根本没把session id发过去,服务端就创建了新session,token自然对不上。

你现在的写法没大问题,但大概率是跨域或cookie没设对导致session丢失。先检查:

- 请求有没有带cookie(Network tab里看request headers里有没有Cookie: PHPSESSID=xxx这种)
- 后端返回的Set-Cookie是不是带了正确的domain/path/secure/samesite
- fetch要不要加credentials: 'include'

如果后端确实想让你手动传token(不依赖session cookie),那后端应该改代码:从header/body里取token,而不是只比对session。这种情况下,你传header是对的,但要确认后端是否真的读了X-CSRF-Token,很多框架默认不读自定义header,得手动配置。

举个最稳妥的方案:
后端在返回登录结果时,把token塞进响应body,前端拿到后写进localStorage/sessionStorage,每次提交时:

- 如果是form表单提交:加一个隐藏字段
- 如果是AJAX:直接塞进body里

比如你这个场景,改成这样直接用这个:

const handleSubmit = async () => {
const token = sessionStorage.getItem('csrfToken');
await fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'test',
csrf_token: token // 注意字段名得和后端约定一致
})
});
};


再让后端从body里取csrf_token字段,和session里的比——99%能通。如果还不行,八成是session没持久化(比如你用的是内存session,服务重启就没了),或者你本地调试没走https导致secure cookie不生效。
点赞 1
2026-02-26 17:08
令狐星宇
你这问题我太熟了,踩过同样坑。问题不在前端发的方式,而在于后端验证逻辑——你后端是不是在验证时,把前端传来的 token 和 session 里存的 token 做了字符串对比,但没做类型转换?或者更常见的情况:你存进 session 的 token 是带引号的 JSON 字符串,比如 "abc123"(注意引号),而你前端拿出来的 sessionStorage.getItem() 返回的是 "abc123",但后端比较时没去掉引号,直接比较 "abc123"abc123,肯定不相等。

不过先别急着改后端,先确认下这个:

你登录接口返回的 token,是直接 {"csrfToken":"xxx"} 这种 JSON,还是只返回 xxx?如果是前者,你 sessionStorage.setItem('csrfToken', res.csrfToken) 这样存的就对;但如果是后端直接 return '"xxx"'(比如某些框架默认序列化),那 session 里存的就是带引号的字符串,前端拿出来的也是带引号的,但后端验证时可能又 trim 了或者做了其他处理,导致不一致。

最稳妥的排查方式:在浏览器 Network 面板里,看 /api/update-profile 请求的 X-CSRF-Token 请求头里,实际传的是什么值;再在后端打印一下 session 里存的值,对比一下两个字符串——多一个空格、多一个引号、少一个换行都可能失败。

如果确认值完全一致但还是失败,那大概率是后端验证逻辑写错了,比如:

// 错误示例(PHP)
if ($_SESSION['csrf_token'] !== $_SERVER['HTTP_X_CSRF_TOKEN']) { ... }


$_SERVER['HTTP_X_CSRF_TOKEN'] 这玩意儿在某些 SAPI 下可能被转成小写或下划线(比如 http_x_csrf_token),或者大小写不一致。建议用 $_REQUEST$_GET/$_POST/$_COOKIE 里统一取,别依赖 header(尤其跨域时 header 可能被浏览器改写)。

更高效的做法:别用 header,直接把 token 放在表单的 hidden 字段里,或者 GET 请求里用 query string(?token=xxx),后端统一从 $_REQUEST 里取,兼容性好、调试方便、还避免 CORS 预检时 header 被过滤的问题。

如果你用的是 Laravel,记得检查 VerifyCsrfToken 中间件的 $except 是否把你的路由漏进去了,或者 SESSION_DRIVERfile 还是 redis,跨请求时 session ID 没传对也会导致取不到同一个 session。

最后说个冷知识:sessionStorage 是按 源(协议+域名+端口)隔离的,你本地开发如果前后端端口不一致(比如前端 3000,后端 8080),sessionStorage.getItem('csrfToken') 拿到的是空值,但你可能没注意到,导致传了 undefined 进 header。建议先 console.log(token) 看一眼是不是空的。

真要我写,我直接这样改前端:

const handleSubmit = async () => {
const token = sessionStorage.getItem('csrfToken');
if (!token) {
console.error('CSRF token not found');
return;
}
console.log('Sending token:', token, 'type:', typeof token); // 调试用
await fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ name: 'test' })
});
};


然后让后端在验证前加一行 log("session token: " + sessionToken + ", header token: " + headerToken),五分钟后就能定位问题。
点赞 1
2026-02-24 21:02