Session绑定CSRF Token后前端怎么正确发送?
我后端用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' })
});
};
常见做法是:后端生成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里
比如你这个场景,改成这样直接用这个:
再让后端从body里取csrf_token字段,和session里的比——99%能通。如果还不行,八成是session没持久化(比如你用的是内存session,服务重启就没了),或者你本地调试没走https导致secure cookie不生效。
"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 里存的值,对比一下两个字符串——多一个空格、多一个引号、少一个换行都可能失败。如果确认值完全一致但还是失败,那大概率是后端验证逻辑写错了,比如:
$_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_DRIVER是file还是redis,跨请求时 session ID 没传对也会导致取不到同一个 session。最后说个冷知识:
sessionStorage是按 源(协议+域名+端口)隔离的,你本地开发如果前后端端口不一致(比如前端 3000,后端 8080),sessionStorage.getItem('csrfToken')拿到的是空值,但你可能没注意到,导致传了undefined进 header。建议先console.log(token)看一眼是不是空的。真要我写,我直接这样改前端:
然后让后端在验证前加一行
log("session token: " + sessionToken + ", header token: " + headerToken),五分钟后就能定位问题。