为什么我的Double Submit Cookie防CSRF方案在登录接口失效?
我在用Double Submit Cookie防CSRF时遇到奇怪的问题:其他接口都正常,但登录接口总提示”CSRF Token mismatch”。我检查了cookie设置和请求头,代码看起来没问题,但就是通不过验证…
我的流程是这样的:服务端在每次响应头里设置csrf_token到cookie,前端在请求时把cookie里的token加到请求头。但登录接口因为需要跨域预检,可能有什么特殊的地方?
这是我的前端代码片段:
document.cookie = <code>csrf_token=${response.csrfToken}; Path=/</code>; // 服务端返回的初始token
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.cookie.match(/csrf_token=([^;]*)/)?.[1] || ''
},
body: JSON.stringify({ username: 'test', password: '123' })
})
.then(res => console.log(res))
.catch(err => console.error('CSRF验证失败:', err));
服务端日志显示请求头里的X-CSRF-Token是空的,但其他接口同样的代码却能拿到正确的token值。难道是登录页面加载时cookie还没生效?或者跨域时没带上cookie?
关键点有三个:
第一,你这行代码
document.cookie = csrf_token=xxx; Path=/是前端自己设的 cookie,但服务端如果在登录响应里又 set-cookie 了新 token,或者你用的是 WordPress 的wp_login()或wp_authenticate()这类函数,它可能在登录成功前就触发了重定向(比如 302 跳转到 dashboard),而重定向时浏览器不会带上刚设的 cookie,尤其是跨域或非 sameSite=none 的情况。第二,也是最坑的:WordPress 的登录接口
/wp-login.php默认不支持 CORS,你如果前端是用fetch跨域请求/login(比如你自建了 API 路由),浏览器在预检请求(OPTIONS)阶段就可能被 WordPress 拦截,或者你的X-CSRF-Token头根本没被转发到 PHP 层。第三,你用
document.cookie.match(...)读 cookie 本身没问题,但问题出在:登录请求发出时,cookie 可能还没写入!因为document.cookie = ...是同步的,但浏览器实际写入 cookie 是有延迟的(尤其在并发请求、或页面刚加载时),你立马发登录请求,大概率读到的是旧值或空值。我之前解决过类似问题,有俩靠谱方案:
一个是在前端加个微小延迟,确保 cookie 生效再发请求,比如:
不过这不优雅,属于临时糊墙。
更干净的做法是:别让前端自己读 cookie,改用服务端返回的 token 直接注入到请求里。比如服务端在响应里直接把 token 写进
或 JS 变量,这样前端拿的时候就不用依赖 cookie 时序:然后前端请求时直接用:
这样就完全绕开了 cookie 读写时序问题。
另外如果你是自己写登录接口(比如用
rest_api_init注册的路由),记得加:并且在 handler 里用
wp_verify_nonce($_REQUEST['_wpnonce'], 'csrf')或者你自己的验证逻辑,别光靠 header。最后提醒一句:WordPress 默认的
nonce是基于 session 的,和 Double Submit Cookie 不是一回事,别混着用。如果真要用 Double Submit Cookie,建议完全绕过 WordPress 的 nonce 体系,自己生成随机 token 存 session / redis,前端读 cookie 验证——但记得把SameSite=None; Secure加上,不然跨域根本带不上 cookie。我当年就是卡在 SameSite 上,搞了两天才发现浏览器默认把 cookie 当 Lax 了……
服务端在处理OPTIONS请求时,应该直接返回200状态码并允许跨域,不需要验证csrf_token。但实际开发中很容易忽略这点,特别是当全局拦截器统一处理csrf校验时。
解决方法是调整服务端逻辑,给OPTIONS请求开个特例。比如这样:
另外提醒一下,设置cookie时最好加上SameSite属性,避免其他潜在问题。还有就是你前端取cookie的正则表达式可以优化下,建议封装成通用方法。
顺便吐槽一句,处理跨域问题真是个体力活,我都记不清踩过多少坑了。记得多测几个浏览器,有时候Chrome能用的方案,Firefox就挂了。