防CSRF攻击案例详解
什么是CSRF攻击?
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式,攻击者诱导用户访问恶意网站,利用用户已登录的身份信息,向目标网站发送伪造的请求,执行非用户意愿的操作。

攻击流程示例
用户登录银行网站,获得有效Cookie
2. 用户未退出银行网站,又访问了恶意网站
3. 恶意网站向银行网站发送转账请求(携带用户Cookie)
4. 银行网站验证用户身份通过,执行转账操作
常见CSRF防御方案对比
| 防御方案 | 安全性 | 用户体验 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| 同步器令牌 | 高 | 完美 | 中等 | 所有场景 |
| 双重Cookie | 高 | 完美 | 简单 | 经典方案 |
| SameSite Cookie | 中高 | 完美 | 极低 | 浏览器支持 |
| 自定义请求头 | 高 | 完美 | 低 | API接口 |
| 二次验证 | 很高 | 较差 | 中等 | 敏感操作 |
案例1:同步器令牌(最常用方案)
前端代码
<!-- 表单中包含CSRF Token -->
<form action="/api/transfer" method="POST" id="transferForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="text" name="amount" placeholder="转账金额">
<button type="submit">确认转账</button>
</form>
// AJAX请求时携带CSRF Token
async function transferMoney(amount) {
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
amount: amount,
_csrf: document.querySelector('input[name="csrf_token"]').value
})
});
return response.json();
}
后端代码(Node.js/Express示例)
// 使用csurf中间件
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// 生成Token并返回给前端
app.get('/form', csrfProtection, (req, res) => {
res.render('transfer', {
csrfToken: req.csrfToken()
});
});
// 验证Token的POST请求
app.post('/api/transfer', csrfProtection, (req, res) => {
// 到这里说明Token验证通过
const { amount } = req.body;
// 执行转账逻辑...
res.json({ success: true });
});
案例2:双重Cookie验证(不占用服务端状态)
工作原理
用户登录时,服务端在Cookie中设置随机Token
2. 前端JS读取Cookie中的Token值
3. 发送请求时,在请求头或请求体中携带该Token
4. 服务端比对Cookie中的Token和请求中携带的Token
实现代码
// 登录成功后设置随机Token
router.post('/login', (req, res) => {
const sessionToken = crypto.randomBytes(32).toString('hex');
// 1. 设置Cookie
res.cookie('csrfToken', sessionToken, {
httpOnly: false, // 必须允许JS读取
secure: true, // HTTPS
sameSite: 'strict'
});
res.json({ success: true });
});
// 全局中间件验证CSRF
app.use('/api/*', (req, res, next) => {
// 2. 从Cookie获取Token
const cookieToken = req.cookies.csrfToken;
// 3. 从请求头或请求体获取Token
const requestToken = req.headers['x-csrf-token'] || req.body.csrfToken;
// 4. 比对
if (cookieToken && requestToken && cookieToken === requestToken) {
next();
} else {
res.status(403).json({ error: 'CSRF验证失败' });
}
});
// 前端JS需要在请求前注入Token
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken')
}
});
案例3:SameSite Cookie(最简单的防御)
// 设置SameSite属性
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict' // 或 'lax'(推荐平衡方案)
});
/*
* strict: 完全禁止第三方Cookie,所有跨站请求都不会携带
* lax: 允许部分安全请求(GET/HEAD/OPTIONS)携带
* none: 完全不做限制(需配合Secure)
*/
注意: 同Site判定标准(2023年后浏览器改为"同源"判定):
- 只关心
scheme://domain - 忽略端口号
- 不同子域被视为跨站
案例4:自定义请求头验证
// 后端验证自定义Header
app.use('/api/*', (req, res, next) => {
const customHeader = req.headers['x-requested-with'];
if (customHeader !== 'XMLHttpRequest') {
return res.status(403).json({ error: '非法请求来源' });
}
// 还可以配合Origin验证
const origin = req.headers.origin;
const allowedOrigins = ['https://bank.example.com'];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: '非允许来源' });
}
next();
});
// 使用场景
fetch('/api/data', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
案例5:二次身份验证(敏感操作)
<!-- 高敏感操作需要重新输入密码 -->
<form action="/api/delete-account" method="POST">
<input type="password" name="password" placeholder="输入密码确认">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">永久删除账户</button>
</form>
// 后端验证
router.post('/api/delete-account', csrfProtection, async (req, res) => {
const { password } = req.body;
// 校验密码
const user = await User.findById(req.session.userId);
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return res.status(403).json({ error: '密码错误,操作取消' });
}
// 执行删除操作
await User.deleteOne({ _id: user._id });
});
实际项目方案组合示例
完整的安全防护层
class CSRFProtection {
constructor() {
this.allowedOrigins = ['https://bank.example.com'];
this.tokenStore = new Map();
}
// 中间件主入口
middleware = async (req, res, next) => {
// 1. 检查请求来源
const origin = req.headers.origin;
if (origin && !this.allowedOrigins.includes(origin)) {
return res.status(403).json({ error: '拒绝来源' });
}
// 2. 检查是否是非简单请求
if (this.isUnsafeMethod(req.method)) {
// 3. 双重验证:Header + 自定义Header
const tokenFromHeader = req.headers['x-csrf-token'];
const tokenFromCookie = req.cookies['csrf-token'];
const tokenFromSession = req.session.csrfToken;
// 三重校验
if (!tokenFromHeader || tokenFromHeader !== tokenFromCookie
|| tokenFromCookie !== tokenFromSession) {
return res.status(403).json({ error: 'Token验证失败' });
}
}
next();
}
isUnsafeMethod(method) {
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
}
// 生成新Token
generateToken(sessionId) {
const token = crypto.randomBytes(32).toString('hex');
this.tokenStore.set(sessionId, token);
return token;
}
}
前端配合方案
// 封装请求,统一注入CSRF Token
class HTTPService {
constructor() {
this.baseURL = '/api';
}
async request(path, options = {}) {
return fetch(`${this.baseURL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.getCSRFToken(),
'X-Requested-With': 'XMLHttpRequest',
...options.headers
},
credentials: 'same-origin'
});
}
getCSRFToken() {
const token = document.cookie.match('(^|;)\\s*csrf-token\\s*=\\s*([^;]+)');
return token ? token.pop() : '';
}
}
总结与最佳实践
三层防御搭配使用
- 第一层(基础防护):SameSite Cookie + 自定义请求头
- 第二层(核心防护):同步器令牌(服务端Session存储)
- 第三层(增强防护):敏感操作二次验证
注意事项
- ✅ Token必须安全随机,使用密码学安全伪随机数
- ✅ Token有效期限制,建议30分钟~2小时
- ✅ 所有非安全方法都必须验证(POST/PUT/DELETE等)
- ✅ 使用HTTPS传输,防止中间人获取Token
- ❌ 不要使用简单随机数生成Token
- ❌ 不要把Token放在URL中(会记录在日志和Referer中)
- ❌ 不要只依赖JSONP(JSONP本身存在CSRF风险)
参考资源