CSRF(跨站请求伪造)是一种利用浏览器信任机制发起攻击的安全漏洞。用户登录某个网站后,浏览器会自动携带Cookie,攻击者只需诱导用户访问恶意页面,就能以用户身份发送伪造请求。在Node.js的Express项目中,如何正确地防御CSRF攻击是每个开发者必须掌握的技能。本文将详细讲解csurf中间件的使用方式,以及更加灵活的双提交Cookie模式的完整实现。

一、什么是CSRF攻击以及为什么需要防护
CSRF攻击的核心在于浏览器的同源策略只限制脚本读取响应,却不限制跨站请求本身携带Cookie。假设用户已经登录了银行网站bank.ippipp.com,此时浏览器保存了该网站的会话Cookie。用户随后访问了一个恶意论坛页面,该页面中嵌入了这样一段HTML代码:
<form action="https://bank.ippipp.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker"/> <input type="hidden" name="amount" value="10000"/> </form> <script>document.forms[0].submit();</script>
当页面加载完成,表单会自动提交。由于请求发往bank.ippipp.com,浏览器会自动附带该域名下的会话Cookie,服务端无法区分这个请求是用户主动发起还是被恶意页面伪造的。如果服务端没有额外的校验机制,转账操作就会被执行。
防御CSRF的思路主要有三种:校验Referer头、使用Synchronizer Token模式、以及双提交Cookie模式。其中Referer校验依赖浏览器行为,可靠性不足;Token校验是目前的主流方案,也是csurf中间件采用的方式;而双提交Cookie模式实现简单,特别适合前后端分离的架构,后面会详细展开。
二、csurf中间件的配置与使用
csurf是Express生态中曾经广泛使用的CSRF防护中间件,它的原理是生成一个随机Token,将其存入Session(或Cookie),并在渲染表单时将Token输出到页面中。表单提交时,服务端比对请求中的Token与存储的Token是否一致,不一致则拒绝请求。
使用csurf需要先安装依赖,典型的安装命令如下:
npm install express csurf cookie-parser express-session
接着在Express应用中进行配置。下面的代码展示了完整的整合方式,包括Session初始化、cookie解析以及csurf中间件的挂载:
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));
// 使用Session存储Token的CSRF中间件
const csrfProtection = csrf({ cookie: false });
app.get('/form', csrfProtection, (req, res) => {
// 将Token传给模板渲染
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
res.send('操作成功');
});
// CSRF校验失败时的错误处理
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).send('CSRF Token校验失败');
}
next(err);
});
app.listen(3000);在前端模板中,需要将Token嵌入到表单的隐藏字段里,csurf默认从_csrf字段或x-csrf-token请求头中读取Token:
<form action="/process" method="POST"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"/> <input type="text" name="username"/> <button type="submit">提交</button> </form>
需要注意的是,csurf这个包已经被官方标记为废弃,不再维护。官方推荐的替代方案是使用csrf-csrf包,它的API设计与csurf类似,但采用了双提交Cookie模式作为默认实现,同时提供了更完善的安全配置项。如果是新项目,建议直接选择后者。
三、双提交Cookie模式的原理与实现
双提交Cookie模式不需要在服务端存储任何状态。服务端生成一个随机Token后,通过Cookie发送给浏览器,同时要求后续的每个请求都在参数或请求头中携带同样的Token。服务端只需比对Cookie中的Token与请求中的Token是否一致即可。由于第三方站点无法读取目标域名的Cookie,也就无法在伪造请求中附带正确的Token,攻击自然无法得逞。
下面是一个不依赖任何第三方库、基于crypto模块手动实现双提交Cookie的例子,代码逻辑清晰且便于理解内部机制:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// 生成Token并写入Cookie
function issueToken(req, res, next) {
if (!req.headers.cookie || !req.headers.cookie.includes('csrfToken=')) {
const token = crypto.randomBytes(32).toString('hex');
// httpOnly必须为false,否则前端JS读不到Token
res.cookie('csrfToken', token, {
httpOnly: false,
sameSite: 'strict',
secure: true
});
// 挂到响应局部变量,方便前端获取
res.locals.csrfToken = token;
}
next();
}
// 校验Cookie与请求头中的Token是否一致
function verifyToken(req, res, next) {
const cookieToken = req.cookies ? req.cookies.csrfToken : null;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF校验失败' });
}
next();
}
app.use(issueToken);
app.post('/api/data', verifyToken, (req, res) => {
res.json({ message: '数据提交成功' });
});
app.listen(3000);前端配合的方式是,在页面加载或首次请求后,从Cookie中读取Token,并在后续的AJAX请求中通过自定义请求头发送。使用fetch的示例如下:
// 从Cookie中读取csrfToken
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrfToken')
},
body: JSON.stringify({ foo: 'bar' })
});四、两种方案的对比与选型建议
csurf代表的Synchronizer Token模式将Token存储在服务端Session中,安全性更高,因为攻击者即使拿到Cookie中的会话标识,也无法伪造出正确的Token。但它要求应用维护Session状态,对无状态API服务或者分布式部署场景不太友好,因为多台服务器之间需要共享Session存储。
双提交Cookie模式的优势在于完全无状态,服务端不需要存储任何数据,天然适合微服务架构和水平扩展。它的安全性依赖于攻击者无法读取跨域Cookie这一前提,只要Token的生成足够随机(建议至少128位熵值),且Cookie设置了合理的sameSite属性,防护效果是有保障的。需要注意的一点是,该模式下Cookie不能设置httpOnly,否则前端脚本无法读取Token,这也意味着如果应用存在XSS漏洞,Token可能被窃取。因此CSRF防护绝不能替代XSS防护,两者需要同时做好。
实际选型时可以这样考虑:传统的服务端渲染项目,使用csrf-csrf包配合模板引擎嵌入Token即可;前后端分离的SPA应用或纯API服务,双提交Cookie模式配合Axios拦截器自动注入请求头,是最简洁高效的方案。此外,无论采用哪种方式,都建议给Cookie设置SameSite=Lax或Strict属性,并在敏感操作上要求二次验证,形成多层防御。安全从来不是单一措施能解决的,纵深防御才是正确的思路。