CSRF(跨站请求伪造)攻击的核心原理是利用用户已经登录的身份凭证,诱导用户在不知情的情况下向目标网站发送恶意请求。而双重提交Cookie方案,就是把CSRF令牌同时放在Cookie和请求体(或请求头)中,服务端通过比对两者是否一致来验证请求的合法性。这种方案不需要服务端存储令牌状态,天然适合前后端分离架构和无状态的API服务,是目前主流的CSRF防护手段之一。
很多开发者在实现CSRF防护时,习惯用传统的Session同步令牌方案,也就是服务端生成一个随机token存到Session里,前端每次提交表单时带上这个token。这种方式虽然可靠,但在分布式系统、微服务架构或者纯API服务中会遇到麻烦——Session共享成本高、无状态服务无法直接使用。双重提交Cookie方案正好解决了这个痛点,下面我们从原理到落地一步步讲清楚。
一、CSRF攻击到底是怎么回事先快速回顾一下CSRF的攻击链路。用户登录了银行网站A,浏览器里保存了A的Cookie。这时候用户不小心访问了恶意网站B,B页面里藏了一段代码,自动向A发起一个转账请求。因为浏览器会自动带上A的Cookie,A的服务器认为这是用户本人的操作,就执行了转账。整个过程用户毫无感知,这就是CSRF的危害。
CSRF攻击能成功的关键在于:浏览器在跨域请求时会自动携带目标域的Cookie,而攻击者可以构造一个诱导用户点击或自动提交的请求。防护的核心思路就是让服务端能够区分"用户主动发起的请求"和"被诱导发起的请求"。
二、双重提交Cookie方案的核心原理双重提交Cookie方案的设计非常巧妙。它的核心逻辑是:
第一步,服务端在用户首次访问时,生成一个随机的CSRF Token,通过Set-Cookie响应头设置到浏览器的Cookie中(注意这个Cookie不需要设置HttpOnly,因为前端JavaScript需要读取它)。
第二步,前端在每次发起需要防护的请求(如POST、PUT、DELETE)时,从Cookie中读取这个Token值,然后把它放到请求体的某个字段中,或者放到自定义的请求头中(比如X-CSRF-Token)。
第三步,服务端收到请求后,从Cookie中取出Token值,再从请求体或请求头中取出Token值,比对两者是否一致。如果一致,说明请求是合法的;如果不一致或者缺失,直接拒绝。
为什么攻击者无法伪造?因为跨域情况下,攻击者的恶意页面无法读取目标域的Cookie(同源策略限制),所以他拿不到Cookie里的Token值,自然也无法在请求体中填入正确的值。即使攻击者尝试猜测Token,由于Token是足够长的随机字符串,暴力破解的概率极低。
三、具体实现步骤与代码示例下面以Node.js + Express为例,展示完整的实现流程。
首先是服务端生成Token并设置Cookie的中间件:
const crypto = require('crypto');
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
function csrfProtection(req, res, next) {
if (!req.cookies['csrf_token']) {
const token = generateCSRFToken();
res.cookie('csrf_token', token, {
httpOnly: false, // 必须设为false,前端JS才能读取
secure: true, // 生产环境开启HTTPS时设为true
sameSite: 'Lax' // 推荐使用Lax或Strict
});
}
next();
}
然后是验证Token的中间件:
function verifyCSRFToken(req, res, next) {
const cookieToken = req.cookies['csrf_token'];
const bodyToken = req.body['csrf_token'] || req.headers['x-csrf-token'];
if (!cookieToken || !bodyToken || cookieToken !== bodyToken) {
return res.status(403).json({
error: 'CSRF token validation failed'
});
}
next();
}
在路由中应用这两个中间件:
app.post('/api/transfer', csrfProtection, verifyCSRFToken, (req, res) => {
// 处理转账逻辑
res.json({ success: true });
});
前端部分,每次发送请求时需要带上Token。以fetch为例:
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
async function makeRequest() {
const csrfToken = getCookie('csrf_token');
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
amount: 100,
to: 'account_123',
csrf_token: csrfToken
})
});
return response.json();
}
四、双重提交Cookie方案的优势与局限
优势方面,这个方案最大的好处是服务端无需存储任何状态。传统的Session同步Token方案要求服务端为每个用户维护一个Token记录,在高并发分布式环境下,Session共享是个大问题。双重提交方案完全规避了这个问题,服务端只需要做一次字符串比对,性能开销极小。
另外,这个方案天然适配RESTful API和SPA(单页应用)。前端框架如Vue、React的Axios拦截器可以很方便地自动读取Cookie并附加Token,开发成本低。
但这个方案也有明显的局限。最大的风险点在于:如果网站存在XSS(跨站脚本)漏洞,攻击者可以通过注入脚本直接读取Cookie中的Token,然后构造带有正确Token的请求,CSRF防护就形同虚设了。所以双重提交Cookie方案必须和XSS防护配合使用,绝不能单独依赖。
另一个局限是Cookie的SameSite属性设置。如果设置为SameSite=Strict,某些正常的跨站导航场景(比如从外部链接跳转到你的网站后发起POST请求)可能会导致Cookie不被发送,从而导致Token验证失败。因此在实际项目中,SameSite=Lax是更稳妥的选择,或者根据业务场景灵活调整。
五、与其他CSRF防护方案的对比目前主流的CSRF防护方案主要有三种:同步Token模式、双重提交Cookie模式、以及SameSite Cookie属性。
同步Token模式是最经典的方案,安全性最高,但需要服务端存储状态,不适合无状态架构。双重提交Cookie模式牺牲了一点点安全性(依赖于没有XSS漏洞),换取了无状态和易实现的优势。SameSite Cookie是浏览器层面的防护,设置为Strict或Lax后可以阻止大部分CSRF攻击,但它不是万能的,对于某些老旧浏览器或者特殊场景仍然可能被绕过。
实际生产环境中,最佳实践是多种方案组合使用。比如:设置Cookie的SameSite=Lax作为第一道防线,同时实现双重提交Token作为第二道防线,再加上严格的XSS防护(CSP策略、输入过滤、输出编码),形成纵深防御体系。
六、生产环境中的注意事项第一,Token的随机性必须足够强。建议使用至少128位(32个十六进制字符)的随机数,不要用时间戳、用户ID等可预测的值拼接。
第二,Token的生命周期要合理。可以每次请求后刷新Token(Rotating Token策略),这样即使Token被截获,攻击者的可用窗口也非常短。刷新逻辑很简单:验证通过后,生成新Token覆盖Cookie即可。
第三,对于GET请求不需要CSRF防护,但要确保GET请求不会产生副作用(不修改数据)。CSRF防护只针对POST、PUT、DELETE等会修改状态的请求。
第四,HTTPS是必须的。如果在HTTP环境下,Cookie可以被中间人截获,双重提交方案的安全性会大打折扣。生产环境务必全站HTTPS。
第五,注意子域问题。如果你的网站有多个子域(如api.example.com和www.example.com),Cookie的Domain属性设置要小心,避免Token被错误地共享到不该共享的子域。
七、总结与建议双重提交Cookie方案是一种实用、高效、适合现代Web架构的CSRF防护手段。它不需要服务端存储,实现简单,性能好,特别适合API服务和前后端分离项目。但它不是银弹,必须配合XSS防护、HTTPS、合理的SameSite设置一起使用才能构建真正安全的防护体系。
对于中小型项目,直接使用这个方案加上基础的XSS过滤就足够了。对于金融、支付等高安全要求的场景,建议在此基础上再叠加同步Token方案和更严格的验证机制,做到多层防护、纵深防御。安全从来不是单一措施能解决的,而是体系化建设的结果。
