请求头验证 CSRF 为啥还是被拦截了?
我在前端用 fetch 发请求时加了自定义请求头 X-Requested-With: XMLHttpRequest,后端也配置了校验这个头,但还是被 CSRF 防护拦住了,到底是哪出问题了?
我试过在 Axios 里也加了同样的头,本地开发环境没问题,一上测试环境就 403。后端同事说他们只认这个头,但浏览器发 OPTIONS 预检之后实际请求好像没带上?
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ data: 'test' })
})
通用的做法是后端需要设置 Access-Control-Allow-Origin 和 Access-Control-Allow-Headers。特别是后者要包含 X-Requested-With 这个自定义头。
在服务器端配置大概这样:
另外注意 OPTIONS 请求是预检请求,浏览器会自动发送,但不会带上你的自定义头。后端要专门处理这个请求返回 200 OK。
还有一个坑就是有些框架的 CSRF 保护默认只对表单提交生效。如果用 token 校验更靠谱些,在请求里加个
X-CSRF-Token头,值从页面获取。这问题折腾起来挺烦人,尤其是环境配置不一致的时候。建议先本地抓包看看请求头是不是完整发出去了。
浏览器发非简单请求(带自定义头)之前会先发OPTIONS预检,这个预检请求可不会带你的X-Requested-With头。后端如果对OPTIONS也拦截,那就直接跪了。
解决方案很简单:后端在CSRF验证那里加个判断,把OPTIONS请求跳过去。
PHP的话大概长这样:
或者用中间件的方式,在CSRF验证逻辑的最前面判断请求方法。
还有个更根本的思路:现在主流做法其实是用SameSite Cookie + CSRF Token,单纯靠请求头判断这种方式本身就有漏洞(因为浏览器正常请求确实会带这个头,但攻击者的恶意请求也可以构造这个头)。如果后端还在用这种方案,建议考虑升级一下防护策略。