为什么我的Double Submit Cookie防CSRF方案在登录接口失效?

UP主~红瑞 阅读 81

我在用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?

我来解答 赞 9 收藏
二维码
手机扫码查看
2 条解答
程序猿星瑶
你这问题我太熟悉了,当年我也栽过一回。问题不在 Double Submit 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 生效再发请求,比如:

setTimeout(() => {
fetch('/login', { /* ... */ });
}, 10);


不过这不优雅,属于临时糊墙。

更干净的做法是:别让前端自己读 cookie,改用服务端返回的 token 直接注入到请求里。比如服务端在响应里直接把 token 写进 或 JS 变量,这样前端拿的时候就不用依赖 cookie 时序:

<script>
window.__CSRF_TOKEN__ = '<?php echo wp_create_nonce("csrf"); ?>';
</script>


然后前端请求时直接用:

fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.__CSRF_TOKEN__
},
body: JSON.stringify({ username: 'test', password: '123' })
});


这样就完全绕开了 cookie 读写时序问题。

另外如果你是自己写登录接口(比如用 rest_api_init 注册的路由),记得加:

add_action('rest_api_init', function() {
register_rest_route('myplugin/v1', '/login', array(
'methods' => 'POST',
'callback' => 'my_login_handler',
'permission_callback' => '__return_true',
));
});


并且在 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 了……
点赞 4
2026-02-24 13:40
司空瑞君
这个问题大概率是跨域请求的预检机制导致的,我来分析一下原因。登录接口涉及到跨域,浏览器在发送POST请求之前会先发一个OPTIONS预检请求,这个预检请求是不会携带cookie的。而你的代码逻辑是在请求头里从cookie里取csrf_token,这就会导致预检阶段token为空。

服务端在处理OPTIONS请求时,应该直接返回200状态码并允许跨域,不需要验证csrf_token。但实际开发中很容易忽略这点,特别是当全局拦截器统一处理csrf校验时。

解决方法是调整服务端逻辑,给OPTIONS请求开个特例。比如这样:

if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token');
return res.status(200).send();
}


另外提醒一下,设置cookie时最好加上SameSite属性,避免其他潜在问题。还有就是你前端取cookie的正则表达式可以优化下,建议封装成通用方法。

顺便吐槽一句,处理跨域问题真是个体力活,我都记不清踩过多少坑了。记得多测几个浏览器,有时候Chrome能用的方案,Firefox就挂了。
点赞 9
2026-02-14 11:01