Double Submit Cookie 防御 CSRF 攻击的实战解析与踩坑经验
为什么我又要折腾 Double Submit Cookie?
最近在给一个老项目加 CSRF 防护,后端同事说“你前端搞个 Double Submit Cookie 就行了”,我一听就头大。不是因为难,而是因为方案太多了,而且每个都有坑。有些看起来简单,结果上线后被 QA 打回来三次;有些理论上很完美,但和我们现有的请求封装冲突得一塌糊涂。
所以这次我干脆把几个主流的实现方式都试了一遍,亲测有效(也踩了不少坑),今天就来聊聊到底哪个更值得用。
核心代码就这几行?别信!
很多人以为 Double Submit Cookie 就是“后端生成 token,前端塞进 header”,但实际开发中,光是 cookie 的读取、发送、同步机制就能让你掉好几根头发。下面我列三个常见方案,都是我实打实用过的。
方案一:纯前端自动读取 + 自动注入(最省事但有雷)
这个方案是我一开始最想用的:后端在登录时种一个叫 XSRF-TOKEN 的 cookie,前端每次发请求前自动读取它,然后塞进 X-XSRF-TOKEN header 里。听起来很优雅,对吧?
// 请求拦截器(比如 axios)
axios.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (token) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token);
}
return config;
});
问题来了:cookie 如果带了 HttpOnly,这段代码直接读不到,等于白写。而如果没设 HttpOnly,又有点安全风险(虽然 Double Submit Cookie 本身不依赖保密性,但很多安全审计还是会卡这一点)。
更头疼的是,有些浏览器在跨域或 iframe 场景下,document.cookie 会静默失败,连报错都没有。我上次就在 Safari 里调试了整整一天,最后发现是第三方 cookie 被拦截了。
所以这个方案,除非你完全控制所有环境(比如内网系统),否则别轻易用。
方案二:后端返回 token,前端手动存 + 发(最可控)
这个是我现在主推的方案。后端在登录接口或专门的 CSRF 接口里,**同时返回 token 值和 Set-Cookie**。前端收到响应后,把 token 存到内存(比如 Vuex、Zustand 或者一个 module 级变量),后续请求从这里取。
// 登录成功后
const login = async () => {
const res = await fetch('/api/login', { method: 'POST' });
const data = await res.json();
// 假设后端返回 { csrfToken: 'abc123' }
storeCsrfToken(data.csrfToken); // 存到全局状态
};
// 请求拦截器
axios.interceptors.request.use(config => {
const token = getCsrfToken(); // 从内存读
if (token) {
config.headers['X-XSRF-TOKEN'] = token;
}
return config;
});
优点很明显:不依赖 document.cookie,绕过了所有浏览器 cookie 限制;token 存在内存里,生命周期清晰,登出时清一下就行;而且和任何请求库都能无缝集成。
缺点?要多一次后端配合,得改接口。但说实话,这点成本比起线上出问题,简直不值一提。我宁愿多写两行后端代码,也不想半夜被告警叫醒。
方案三:服务端渲染(SSR)专属方案(小众但稳)
如果你用的是 Next.js、Nuxt 这类 SSR 框架,还有一种骚操作:在服务端渲染时,把 CSRF token 注入到页面的 <meta> 标签里,前端直接读 DOM。
<!-- 服务端生成 -->
<meta name="csrf-token" content="abc123">
// 前端
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
这个方案在 SSR 场景下非常稳,因为 token 是 HTML 的一部分,不存在异步加载或 cookie 限制的问题。但前提是你的首屏必须是服务端渲染,如果是 CSR(客户端渲染)为主的 SPA,那这招就废了。
另外,如果页面被缓存(比如 CDN 缓存了 HTML),token 可能过期,得配合 cache-control 控制。我之前在一个电商项目里试过,结果促销期间 CDN 缓存没刷新,导致大量用户提交失败……血的教训。
谁更灵活?谁更省事?
说白了,选型就看两点:你的项目架构 + 你能控制多少后端。
- 如果你做的是纯前端 SPA,且后端愿意配合返回 token,方案二闭眼选。它不依赖浏览器特性,调试方便,出问题一眼就能定位。
- 如果你是内网系统、不用考虑跨域、且能确保 cookie 不被拦截,方案一可以图个快。但上线前务必在 Safari、Firefox、Edge 全测一遍。
- SSR 项目?方案三很香,但记得关掉 HTML 缓存,或者用动态 token(比如每次请求都刷新 meta 标签,但这又复杂了)。
至于我?我现在所有新项目都用方案二。虽然多写几行代码,但省心。毕竟 CSRF 防护不是功能,是底线,宁可啰嗦点,也不能出岔子。
踩坑提醒:这三点一定注意
1. **token 同步问题**:如果用户开多个 tab,其中一个 tab 登出,其他 tab 的 token 就失效了。这时候要么做心跳检测,要么在 401/403 响应里统一处理跳转。别指望 Double Submit Cookie 自己解决会话同步。
2. **CORS 配置**:如果你的 API 是跨域的,记得后端要设置 Access-Control-Allow-Credentials: true,并且前端 fetch 要加 credentials: 'include'。不然 cookie 根本带不上去,token 也收不到。
3. **不要混淆 SameSite**:有些人以为开了 SameSite=Strict 就不用 CSRF 防护了,这是误区。虽然 SameSite 能防大部分 CSRF,但老浏览器不支持,而且某些 POST 跳转场景依然有风险。Double Submit Cookie 是兜底,不是替代。
我的选型逻辑
总结一下我的偏好:
优先级:方案二 > 方案三 > 方案一
理由很简单:方案二把控制权握在自己手里,不依赖浏览器玄学行为,也不怕 CDN 缓存捣乱。虽然要后端多返回一个字段,但沟通成本远低于线上故障的修复成本。
而且,方案二还能轻松扩展:比如 token 快过期时,自动调用刷新接口;或者在测试环境 mock token。灵活性拉满。
以上是我个人对 Double Submit Cookie 几种实现方式的完整踩坑总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合 JWT),后续会继续分享这类实战博客。

暂无评论