CSRF(跨站请求伪造)防御实战:深入理解 SameSite 属性与 Token 机制
大家好,欢迎来到今天的网络安全技术讲座。今天我们聚焦一个在 Web 安全领域中极其重要但又常被忽视的问题——跨站请求伪造(Cross-Site Request Forgery, CSRF)。
如果你是一名开发者、运维人员或安全工程师,那么你一定听说过 CSRF。它不像 SQL 注入那样直接导致数据泄露,也不像 XSS 那样能执行恶意脚本,但它却能在用户不知情的情况下,完成“看起来合法”的操作,比如转账、修改密码、删除账户等。一旦攻击成功,后果可能非常严重。
一、什么是 CSRF?为什么它危险?
定义
CSRF 是一种利用用户已登录的身份,在未经其同意的情况下,向目标网站发送恶意请求的攻击方式。
举个例子:
- 用户登录了银行网站 A(如
https://bank.example.com),浏览器保存了 Cookie(如sessionid=abc123)。 - 用户访问了一个恶意网站 B(如
https://malicious-site.com)。 - 恶意网站 B 的 HTML 或 JavaScript 发起一个 GET/POST 请求到银行网站 A,例如:
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" />
这个请求会自动带上用户的 Cookie(因为同域策略允许),银行服务器认为这是合法请求,于是执行转账操作!
⚠️ 关键点:攻击者不需要知道用户的密码、Cookie 内容或其他敏感信息,只需要诱导用户访问恶意页面即可。
二、传统防御手段及其局限性
早期常见的防御方法包括:
| 方法 | 描述 | 缺点 |
|---|---|---|
| Referer 检查 | 检查 HTTP 头中的 Referer 是否来自可信域名 | 可被伪造,部分浏览器禁用 Referer(如隐私模式) |
| Captcha | 强制用户输入验证码 | 影响用户体验,无法阻止自动化工具 |
| 登录二次确认 | 所有敏感操作都需要重新验证身份 | 增加交互复杂度,易被绕过 |
这些方案要么不彻底,要么牺牲可用性。因此,现代 Web 应用更推荐使用两种成熟且高效的防御机制:SameSite Cookie 属性 和 CSRF Token(令牌)。
三、SameSite Cookie 属性:从源头切断攻击路径
什么是 SameSite?
SameSite 是一种新的 Cookie 属性,由 RFC 6265 补充定义,用于限制 Cookie 在跨站请求中是否发送。
它的值可以是以下三种之一:
| SameSite 值 | 含义 | 行为描述 |
|---|---|---|
Strict |
严格模式 | Cookie 仅在同站点请求时发送(即请求 URL 和 Cookie 所属域名完全一致) |
Lax |
松散模式 | 主要允许 GET 请求(如链接点击)携带 Cookie,其他 POST 请求不会带 |
None |
无限制 | 允许跨站发送,但必须同时设置 Secure(HTTPS) |
⚠️ 注意:若未显式指定 SameSite,默认行为是
Lax(Chrome 84+ 开始生效)
实战代码示例(Node.js + Express)
假设我们有一个简单的登录接口:
app.post('/login', (req, res) => {
// 假设这里验证了用户名和密码
const user = authenticateUser(req.body.username, req.body.password);
if (user) {
res.cookie('sessionid', user.sessionId, {
httpOnly: true,
secure: true, // 必须 HTTPS 才能传输
sameSite: 'Lax', // 推荐使用 Lax,平衡安全性与兼容性
maxAge: 3600 * 1000 // 1小时过期
});
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
此时,如果用户访问恶意网站 B,尝试发起对银行网站 A 的请求:
<!-- 恶意网站 B -->
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" />
由于 sameSite=Lax,该请求不会携带 Cookie!攻击失败。
✅ 优点:
- 简单有效,无需前端额外逻辑;
- 对大多数主流浏览器支持良好(Chrome、Firefox、Safari 均支持);
❌ 局限:
- 如果你的应用需要跨站 API 调用(如第三方嵌入 iframe),则需设为
None并配合Secure; - 不适用于所有场景(比如某些 OAuth 流程依赖跨站重定向);
四、CSRF Token:更细粒度的防护机制
SameSite 虽然强大,但在某些情况下仍不够。比如:
- 某些旧浏览器不支持 SameSite;
- 你需要允许跨站 AJAX 请求(如微前端架构);
- 你想对每个请求做精确控制(如不同页面有不同的 token);
这时就需要引入 CSRF Token。
核心思想
在每次表单提交或 AJAX 请求中加入一个随机生成的 token,服务器端校验该 token 是否匹配。
实现步骤如下:
- 服务端生成 token 并存储
- 前端渲染时注入 token(HTML 或 JS)
- 每次请求附带 token(通常放在 Header 或 Form Data)
- 后端验证 token 是否正确
示例代码(Express + EJS 模板引擎)
Step 1:生成并存储 token(可存入 session 或 Redis)
// middleware/csrfToken.js
const crypto = require('crypto');
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
// 存储 token 到 session(实际项目建议用 Redis)
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCsrfToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
Step 2:模板中注入 token(EJS)
<!-- views/index.ejs -->
<form action="/transfer" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<label>To:</label>
<input type="text" name="to" />
<label>Amount:</label>
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>
Step 3:后端校验 token(中间件)
// middleware/csrfCheck.js
function csrfCheck(req, res, next) {
const clientToken = req.body._csrf || req.headers['x-csrf-token'];
const serverToken = req.session.csrfToken;
if (!clientToken || clientToken !== serverToken) {
return res.status(403).json({ error: 'CSRF token mismatch' });
}
// 校验通过,清除 token(防止重放攻击)
req.session.csrfToken = null;
next();
}
// 应用中间件
app.use('/transfer', csrfCheck);
这样即使攻击者诱导用户点击恶意链接,也无法获取正确的 _csrf 字段,从而无法伪造请求。
✅ 优点:
- 精确控制每一个请求;
- 支持跨站 AJAX(只要客户端主动传 token);
- 易于集成到现有框架(React/Vue/Angular 均可轻松实现);
❌ 缺点:
- 需要前后端配合开发;
- 若忘记校验,仍有风险;
- Token 管理复杂(需防重放、定期刷新);
五、组合使用 SameSite + Token:最佳实践
理想情况下,我们应该将两者结合使用:
| 场景 | SameSite 设置 | Token 使用 | 说明 |
|---|---|---|---|
| 普通网页表单提交 | Lax |
✅ 必须 | SameSite 阻止大部分攻击,Token 提供额外保障 |
| AJAX 请求(跨站) | None + Secure |
✅ 必须 | SameSite 不再适用,必须靠 Token 校验 |
| 微前端或 iframe 嵌套 | None + Secure |
✅ 必须 | 同上,token 是唯一防线 |
| 移动 App 内嵌 WebView | Strict 或 Lax |
❗ 可选 | 若 App 自带 Cookie 管理,可简化处理 |
推荐配置(Express 示例)
// 设置全局 Cookie 安全策略
app.use((req, res, next) => {
res.cookie('sessionid', req.session.id, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: 3600 * 1000
});
// 对敏感路由启用 CSRF Token 校验
if (req.path.startsWith('/api/') || req.path === '/transfer') {
csrfCheck(req, res, next);
} else {
next();
}
});
这种组合既能享受 SameSite 的便捷,又能利用 Token 的灵活性,真正构建“纵深防御”。
六、常见误区与注意事项
| 误区 | 正确做法 |
|---|---|
| “我用了 SameSite 就不用 Token 了” | SameSite 不能覆盖所有场景(如跨站 AJAX),Token 是必要补充 |
| “我把 token 放在 localStorage 就安全了” | localStorage 可被 XSS 攻击窃取,应优先使用内存变量或隐藏字段 |
| “Token 一次就失效,永远不重复” | 应设计为一次性 token,但允许短期缓存(如 5 分钟内有效) |
| “只检查 Referer 就够了” | Referer 可伪造,不可作为主要防护手段 |
此外,还需注意以下几点:
- 避免暴露 token 给第三方脚本(如 CDN 加载的外部 JS);
- 定时刷新 token(尤其是长时间停留页面);
- 日志记录异常请求(便于事后追踪);
- 测试工具辅助检测(如 Burp Suite、OWASP ZAP);
七、总结与建议
CSRF 是一种隐蔽性强、危害大的攻击方式,但我们可以通过合理的防御策略将其扼杀在摇篮中。
✅ 最佳实践总结:
| 技术 | 推荐用途 | 是否必备 |
|---|---|---|
| SameSite Cookie 属性 | 默认防护,尤其适合表单提交 | ✅ 强烈推荐 |
| CSRF Token | 敏感操作、AJAX、跨站请求 | ✅ 必备 |
| Secure + HttpOnly | 所有 Cookie 设置 | ✅ 必须 |
| Token 清除机制 | 防止重放攻击 | ✅ 必须 |
记住一句话:“不要依赖单一防御,要用多层防护构建信任链。”
最后提醒一句:无论多么复杂的系统,安全的第一道防线永远是你自己的意识——保持更新知识、关注漏洞公告、定期审计代码,才是真正的高手之道。
感谢大家的聆听!希望今天的分享对你今后的设计与开发有所帮助。如有疑问,欢迎留言讨论!