CSRF 攻击原理与防御:SameSite Cookie 属性与自定义 Header 的双重保障
各位开发者朋友,大家好!今天我们来深入探讨一个在 Web 安全领域中非常关键但又常被忽视的问题——跨站请求伪造(CSRF)攻击。我们将从攻击原理讲起,逐步揭示其危害性,并重点介绍两种现代且有效的防御机制:SameSite Cookie 属性 和 自定义 HTTP Header(如 X-Requested-With 或基于 JWT 的 Token 机制)。最后,我会通过实际代码演示如何结合这两种方式构建更安全的系统。
一、什么是 CSRF?它为什么危险?
CSRF(Cross-Site Request Forgery),即“跨站请求伪造”,是一种利用用户已登录的身份,在用户不知情的情况下,诱使浏览器向目标网站发送恶意请求的攻击方式。
✅ 攻击场景举例:
假设你正在使用银行网站(https://bank.example.com),并成功登录。此时你的浏览器保存了该站点的认证 Cookie(比如 sessionid=abc123)。
然后你访问了一个恶意网站(https://evil.com),这个网站嵌入如下 HTML:
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" />
当你的浏览器加载这张图片时,会自动带上你对 bank.example.com 的 Cookie,于是银行服务器误以为这是你本人发起的转账请求!
📌 注意:这并不需要你点击任何按钮,只要你在银行网站登录过,且没有退出登录,就可能触发此攻击。
这种攻击的危害在于:
- 用户无感知地执行敏感操作(如转账、修改密码)
- 利用的是用户身份信任,而非破解密码或窃取凭证
- 很难通过传统防火墙检测(因为请求看起来合法)
二、CSRF 攻击的常见载体
| 攻击类型 | 描述 | 是否需用户交互 |
|---|---|---|
| 图片标签 | <img> 加载远程资源 |
❌ 不需要 |
| 表单提交 | <form action="..." method="POST"> |
✅ 需要点击按钮或自动提交 |
| JavaScript 脚本 | 使用 fetch() / XMLHttpRequest 发送请求 |
✅ 可以静默执行 |
虽然图片标签是最简单的形式,但在现代前端开发中,JavaScript 更加常用。因此,我们必须考虑所有可能性。
三、防御策略:从源头到中间层的双保险
我们不能只依赖单一手段。理想的做法是采用 多层防御体系,其中最核心的就是:
- SameSite Cookie 属性 —— 浏览器层面阻止跨域请求携带 Cookie;
- 自定义 Header 校验 —— 后端强制要求特定 Header,防止非同源 JS 发起请求。
下面我们逐个详解。
四、SameSite Cookie 属性:让浏览器帮你挡掉跨站请求
🔍 原理说明:
SameSite 是一个 Cookie 的属性字段,用于控制浏览器是否会在跨站请求中附带该 Cookie。
它的三种值分别是:
| SameSite 值 | 行为描述 | 适用场景 |
|---|---|---|
Strict |
只允许同站请求携带 Cookie(即使是 iframe 也不行) | 最严格,适合高安全性接口(如登录态) |
Lax |
允许 GET 请求(如导航链接)携带 Cookie,但禁止 POST 等非幂等请求 | 平衡体验与安全,推荐多数场景 |
None |
不限制跨站携带,但必须同时设置 Secure(仅 HTTPS) |
用于第三方嵌入(如 OAuth 登录回调) |
⚠️ 如果你不设置
SameSite,默认行为等于None,意味着任意域名都可以读取你的 Cookie!
🧪 示例:设置 SameSite Cookie(Node.js + Express)
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // 必须 HTTPS
httpOnly: true, // 防止 XSS 获取 Cookie
sameSite: 'lax' // 关键!防 CSRF
}
}));
如果你用的是 Python Flask:
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'your-secret-key'
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # 设置 SameSite
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS 才传 Cookie
✅ 效果:
- 当用户访问
https://evil.com时,浏览器不会将bank.example.com的 Cookie 发送给恶意网站。 - 即使恶意页面尝试用
<img>或 JS 请求银行接口,也不会带上认证信息!
💡 提示:SameSite=Lax 是目前最佳实践,既能保护大多数情况,又不影响正常的导航跳转。
五、自定义 Header 校验:后端主动识别“是不是我自己发的请求”
即使设置了 SameSite,也不能完全杜绝风险(例如某些旧浏览器不支持 SameSite,或者 API 被嵌入到 iframe 中)。这时候就需要 应用层校验 —— 强制要求客户端发送一个特殊的 Header。
💡 常见做法:
方法一:使用 X-Requested-With Header(简单但有效)
前端在发起 AJAX 请求时添加:
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // 关键!
},
body: JSON.stringify({ to: 'attacker', amount: 1000 })
});
后端验证:
app.use((req, res, next) => {
if (req.method === 'POST' && !req.get('X-Requested-With')) {
return res.status(403).json({ error: 'Invalid request origin' });
}
next();
});
✅ 优点:轻量级,兼容性强
❌ 缺点:容易被绕过(如果攻击者也能伪造该 Header)
方法二:使用 JWT Token + 自定义 Header(推荐)
更高级的方式是使用 JWT(JSON Web Token)作为请求标识,配合自定义 Header(如 Authorization: Bearer <token>)进行双向验证。
步骤如下:
- 用户登录成功后,服务端返回一个 JWT Token(可放在 localStorage 或内存中);
- 每次请求都带上这个 Token(通过
AuthorizationHeader); - 后端校验 Token 是否有效,且来源是否可信(比如检查签发时间、签名等);
示例代码(Express + JWT):
const jwt = require('jsonwebtoken');
// 登录接口生成 Token
app.post('/login', (req, res) => {
const { username, password } = req.body;
// ... 验证用户名密码 ...
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
// 中间件校验 Token
function auth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// 受保护路由
app.post('/transfer', auth, (req, res) => {
// 此时 req.user 已经存在,可以安全处理业务逻辑
console.log(`User ${req.user.userId} is transferring money`);
res.json({ success: true });
});
📌 这种方式的优势:
- Token 可绑定用户 ID、过期时间、IP 地址等信息;
- 即使 Cookie 被盗(如 XSS),也无法伪造合法 Token;
- 结合
SameSiteCookie,形成双重防护。
六、实战对比:不同配置下的安全性差异
| 防御措施 | 是否能防止 CSRF | 是否影响用户体验 | 推荐程度 |
|---|---|---|---|
| 无任何防护 | ❌ 完全无效 | ✅ 无影响 | ⚠️ 不可用 |
| Only SameSite=Lax | ✅ 大部分有效 | ✅ 几乎无影响 | ✅ 推荐基础配置 |
Only Custom Header (X-Requested-With) |
✅ 对于纯 JS 请求有效 | ✅ 无影响 | ✅ 建议补充 |
| SameSite + JWT Token | ✅ 极高安全性 | ✅ 有轻微学习成本 | ✅ 最佳实践 |
| SameSite + CSRF Token(隐藏字段) | ✅ 传统方案 | ❌ 需手动插入 hidden input | ⚠️ 逐渐淘汰 |
✅ 综合建议:使用
SameSite=Lax+ JWT Token 校验 是当前最合理的组合,既符合现代标准,又能应对各种潜在威胁。
七、常见误区与注意事项
❗ 误区 1:“我用了 HTTPS 就不怕 CSRF”
错误!HTTPS 只保证传输加密,无法防止跨站请求伪造。攻击依然可以通过恶意脚本诱导用户操作。
❗ 误区 2:“我设置了 HttpOnly Cookie 就万无一失”
也错!HttpOnly 只防 XSS 盗取 Cookie,但不能防 CSRF(因为攻击者可以用正常方式触发请求)。
❗ 误区 3:“只要禁用 Cookie 就能解决 CSRF”
荒谬!很多功能依赖 Cookie(如 Session),完全禁用会导致无法登录,反而更糟。
✅ 正确做法:
- 不要依赖单一防御机制
- 优先使用浏览器原生能力(SameSite)
- 辅以应用层校验(Token)
- 定期更新依赖库(避免已知漏洞)
八、总结:打造抗 CSRF 的现代 Web 应用
今天我们系统地讲解了:
- CSRF 攻击的本质:利用用户身份执行未经授权的操作;
- SameSite Cookie 属性的作用:浏览器级别拦截跨域请求;
- 自定义 Header + JWT Token 的增强作用:应用层二次确认请求来源;
- 最佳实践组合:
SameSite=Lax + JWT Token,兼顾安全性和易用性。
记住一句话:
“安全不是靠某一个特性实现的,而是靠多个层次的协同防御。”
无论你是做前后端开发、运维部署还是架构设计,都应该把 CSRF 防护纳入日常规范。希望今天的分享对你有所启发,让我们一起写出更安全的代码!
✅ 下一步你可以做的:
- 在项目中启用
SameSite=Lax - 引入 JWT Token 替代纯 Cookie 认证
- 添加中间件校验
AuthorizationHeader - 使用工具扫描是否存在 CSRF 漏洞(如 OWASP ZAP)
感谢聆听!如有疑问欢迎留言讨论。