CSRF(跨站请求伪造)是网站安全中最常见也最容易被忽视的攻击方式之一。攻击者利用用户已登录的身份,诱导浏览器向目标网站发送非用户本意的请求,从而完成转账、修改密码、删除数据等恶意操作。防御CSRF的核心手段有两个:一是在每个表单或请求中嵌入随机生成的CSRF令牌(Token),服务端验证令牌的合法性;二是通过同源策略(Same-Origin Policy)配置,限制跨域请求的来源。这两项措施必须配合使用,才能构建起可靠的防护体系。下面我会从令牌生成机制、服务端验证逻辑、同源检查配置、以及实际部署中的注意事项,逐一拆解。
一、CSRF令牌的生成原理与最佳实践
CSRF令牌本质上是一段不可预测的随机字符串,它与用户会话绑定,每次请求都需要携带这个令牌。生成令牌时,必须满足三个条件:足够长(至少32字节)、足够随机(使用密码学安全的随机数生成器)、与用户会话唯一关联。很多开发者用简单的MD5或者时间戳拼接来生成令牌,这种做法在现代攻击面前已经不够安全了。
在Node.js环境下,推荐使用crypto模块的randomBytes方法生成令牌:
const crypto = require('crypto');
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// 将令牌存入session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
next();
});
在Java Spring框架中,可以通过内置的CsrfTokenRepository来自动生成和管理令牌,开发者只需要在模板中渲染令牌即可。在Python Flask或Django中,也有对应的中间件自动处理令牌的生成与校验。关键是不要自己造轮子,优先使用框架提供的成熟方案。
二、令牌的传递方式与前端嵌入
令牌生成后,需要在每次请求时传递给服务端。常见的传递方式有三种:隐藏表单字段、自定义请求头、以及Cookie同步模式。对于传统的表单提交(POST表单),最简单的方式是在HTML表单中加入一个隐藏字段:
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<input type="text" name="amount">
<button type="submit">确认转账</button>
</form>
对于AJAX请求,推荐通过自定义请求头传递令牌,例如使用X-CSRF-Token头部。前端代码示例如下:
fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ name: 'new_name' })
});
这里有一个细节很多人会忽略:令牌应该同时放在页面的meta标签中,方便JavaScript统一读取。在模板引擎中渲染页面时,把令牌写入meta标签是一个标准化的做法。
三、服务端验证逻辑的实现要点
服务端收到请求后,必须从请求体或请求头中提取令牌,并与session中存储的令牌进行比对。比对时要使用恒定时间比较函数(constant-time comparison),防止时序攻击(timing attack)。如果令牌不匹配或缺失,直接返回403 Forbidden状态码,不要返回200。
以下是一个Express中间件的验证示例:
const crypto = require('crypto');
function csrfProtection(req, res, next) {
const tokenFromHeader = req.headers['x-csrf-token'];
const tokenFromBody = req.body._csrf;
const sessionToken = req.session.csrfToken;
const clientToken = tokenFromHeader || tokenFromBody;
if (!clientToken || !sessionToken) {
return res.status(403).json({ error: 'CSRF token missing' });
}
// 使用恒定时间比较
if (!crypto.timingSafeEqual(
Buffer.from(clientToken),
Buffer.from(sessionToken)
)) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
}
需要特别注意的是,GET请求不应该触发CSRF防护逻辑,因为GET请求本身不应该修改数据。但如果你的系统中存在用GET做修改操作的情况(这本身就是设计缺陷),那就需要对所有请求都做校验。另外,令牌在用户每次登录或会话刷新时应该重新生成,避免令牌被长期复用带来的风险。
四、同源检查(Same-Origin Policy)的配置策略
同源策略是浏览器的核心安全机制,它规定了一个页面只能向与自身同源(协议+域名+端口完全一致)的地址发起请求。但在实际开发中,我们经常需要跨域通信,这时候就需要通过CORS(跨域资源共享)头来显式控制哪些来源可以访问资源。正确配置CORS头是同源检查的关键环节。
在Nginx中配置CORS的基本方式如下:
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
# 明确指定允许的来源,不要使用通配符 *
add_header 'Access-Control-Allow-Origin' 'https://www.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, X-CSRF-Token';
add_header 'Access-Control-Allow-Credentials' 'true';
# 预检请求缓存
add_header 'Access-Control-Max-Age' 3600;
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend;
}
}
这里有几个关键点:第一,Access-Control-Allow-Origin不要设置为*,尤其是在需要携带Cookie(凭证)的场景下,浏览器会直接拒绝。第二,Access-Control-Allow-Headers必须包含你自定义的CSRF令牌头部名称。第三,预检请求(OPTIONS)要单独处理并快速返回,避免浪费服务器资源。
五、双重Cookie防御模式的补充说明
除了令牌验证和CORS配置,还有一种被称为"双重Cookie"的防御模式。其原理是:服务端在响应中设置一个Cookie,同时要求前端在请求时从Cookie中读取值并放入请求头或请求体中。由于攻击者无法读取目标域的Cookie(同源策略限制),所以无法伪造这个值。这种方式可以作为令牌机制的补充,但不能完全替代令牌验证。
// 服务端设置双重Cookie
res.cookie('csrf_cookie', csrfToken, {
httpOnly: false, // 必须设为false,让JS能读取
sameSite: 'Strict',
secure: true
});
// 前端读取并附加到请求
const csrfCookie = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_cookie='))
.split('=')[1];
fetch('/api/action', {
headers: { 'X-CSRF-Token': csrfCookie }
});
需要注意的是,sameSite属性设置为Strict或Lax可以进一步防止Cookie在跨站场景下被发送。但如果你的业务需要跨站携带Cookie,就必须配合CORS和令牌机制一起使用。
六、实际部署中容易踩的坑
第一个坑是令牌泄露。如果页面中存在XSS漏洞,攻击者可以通过JavaScript读取页面上的CSRF令牌,那么令牌机制就形同虚设。所以CSRF防护必须和XSS防护同步进行,内容安全策略(CSP)头也要正确配置。
第二个坑是忽略了API接口的防护。很多团队只在传统表单页面做了CSRF防护,但忘记了RESTful API接口同样需要防护。尤其是使用JSON格式提交的接口,必须通过请求头传递令牌。
第三个坑是令牌没有与会话绑定。如果令牌是全局唯一的而不是与用户会话绑定,那么一个用户的令牌可能被另一个用户利用。每个用户的会话必须有独立的令牌。
第四个坑是缓存问题。如果页面被缓存(CDN或浏览器缓存),缓存的页面中包含的令牌可能是过期的或属于其他用户的。对于包含CSRF令牌的页面,必须设置Cache-Control: no-store或no-cache。
七、总结与行动建议
CSRF防护不是单一措施能解决的问题,它需要令牌生成、令牌传递、服务端验证、同源策略配置、Cookie安全属性、以及XSS防护等多层机制协同工作。对于任何一个生产环境的网站,我的建议是:优先使用成熟框架自带的CSRF中间件,不要自己从零实现;同时在Web服务器层面正确配置CORS头和安全响应头;定期进行安全扫描和渗透测试,验证防护措施的有效性。安全不是一次性的工作,而是持续迭代的过程。
