您现在的位置是:网站首页 > 双重提交 Cookie 方案文章详情
双重提交 Cookie 方案
陈川
【
前端安全
】
60136人已围观
8220字
什么是双重提交 Cookie 方案
双重提交 Cookie 方案是一种用于防范跨站请求伪造(CSRF)攻击的安全机制。它的核心思想是让客户端在发送请求时携带两个相同的令牌:一个通过 Cookie 发送,另一个通过请求体或头部发送。服务器通过比较这两个令牌是否一致来验证请求的合法性。
工作原理
- 令牌生成:服务器在用户首次访问时生成一个随机令牌,并将其存储在用户的会话中
- Cookie 设置:服务器将该令牌通过 Set-Cookie 头部发送给客户端
- 表单嵌入:服务器在返回的 HTML 表单中包含一个隐藏字段,其值也是这个令牌
- 请求发送:客户端提交表单时,会自动携带 Cookie 中的令牌,同时也会提交表单中的令牌
- 服务器验证:服务器比较这两个令牌是否匹配,以此判断请求是否合法
实现示例
以下是使用 Node.js 和 Express 实现双重提交 Cookie 方案的代码示例:
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();
app.use(cookieParser());
// 生成随机令牌
function generateToken() {
return crypto.randomBytes(16).toString('hex');
}
// 中间件:验证双重提交 Cookie
function verifyDoubleSubmitCookie(req, res, next) {
const cookieToken = req.cookies.csrf_token;
const bodyToken = req.body.csrf_token;
if (!cookieToken || !bodyToken || cookieToken !== bodyToken) {
return res.status(403).send('CSRF token验证失败');
}
next();
}
// 设置路由
app.get('/form', (req, res) => {
const token = generateToken();
res.cookie('csrf_token', token, { httpOnly: true });
res.send(`
<form action="/submit" method="POST">
<input type="hidden" name="csrf_token" value="${token}">
<input type="text" name="data">
<button type="submit">提交</button>
</form>
`);
});
app.post('/submit', verifyDoubleSubmitCookie, (req, res) => {
res.send('表单提交成功');
});
app.listen(3000);
安全性分析
双重提交 Cookie 方案相比传统的 CSRF 防护方法有几个优势:
- 无需服务器端存储:令牌只需在 Cookie 和表单中保持一致,不需要服务器维护状态
- 简单实现:不需要复杂的加密或签名机制
- 兼容性好:适用于各种前端框架和技术栈
但也要注意以下安全考虑:
- Cookie 必须设置为 HttpOnly 和 Secure(如果使用 HTTPS)
- 令牌必须有足够的随机性,防止被猜测
- 对于敏感操作,建议结合其他安全措施
实际应用场景
- 传统表单提交:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="生成的令牌">
<input type="text" name="amount">
<input type="text" name="account">
<button type="submit">转账</button>
</form>
- AJAX 请求:
// 从 Cookie 中读取 CSRF 令牌
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// 发送 AJAX 请求
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token')
},
body: JSON.stringify({
amount: 1000,
account: '123456',
csrf_token: getCookie('csrf_token')
})
});
与其他方案的比较
-
同步令牌模式:
- 需要服务器存储令牌
- 实现复杂度较高
- 适用于所有类型的请求
-
双重提交 Cookie:
- 无需服务器存储
- 实现简单
- 依赖 Cookie 功能
-
SameSite Cookie:
- 浏览器原生支持
- 兼容性问题
- 不能完全替代 CSRF 防护
最佳实践建议
- 令牌生成:
// 使用加密安全的随机数生成器
function generateStrongToken() {
return crypto.randomBytes(32).toString('hex');
}
- Cookie 设置:
res.cookie('csrf_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 1天
});
- 多页面应用处理:
// 在全局 JavaScript 中设置 AJAX 默认头部
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', getCookie('csrf_token'));
}
});
常见问题解决
-
Cookie 被禁用:
- 提供降级方案
- 显示错误提示
- 考虑使用 localStorage 替代方案
-
多标签页问题:
- 每个页面生成独立令牌
- 或者使用持久化的主令牌
-
API 请求处理:
// 中间件:支持多种令牌提交方式
function flexibleCSRFCheck(req, res, next) {
const cookieToken = req.cookies.csrf_token;
const bodyToken = req.body.csrf_token;
const headerToken = req.headers['x-csrf-token'];
const submittedToken = bodyToken || headerToken;
if (!cookieToken || !submittedToken || cookieToken !== submittedToken) {
return res.status(403).json({ error: 'CSRF验证失败' });
}
next();
}
性能考虑
-
令牌生成开销:
- 对于高并发应用,考虑预生成令牌池
- 使用性能更好的随机数生成器
-
验证逻辑优化:
// 快速失败验证
function quickVerify(cookieToken, submittedToken) {
if (!cookieToken || !submittedToken) return false;
if (cookieToken.length !== submittedToken.length) return false;
return crypto.timingSafeEqual(
Buffer.from(cookieToken),
Buffer.from(submittedToken)
);
}
框架集成示例
- React 集成:
import React, { useEffect } from 'react';
import axios from 'axios';
function ProtectedForm() {
useEffect(() => {
const token = document.cookie.replace(
/(?:(?:^|.*;\s*)csrf_token\s*=\s*([^;]*).*$)|^.*$/,
'$1'
);
axios.defaults.headers.common['X-CSRF-Token'] = token;
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
const token = document.cookie.match(/csrf_token=([^;]+)/)[1];
await axios.post('/api/data', {
data: e.target.data.value,
csrf_token: token
});
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="data" />
<button type="submit">提交</button>
</form>
);
}
- Vue 集成:
// main.js
import Vue from 'vue';
import axios from 'axios';
const token = document.cookie.replace(
/(?:(?:^|.*;\s*)csrf_token\s*=\s*([^;]*).*$)|^.*$/,
'$1'
);
axios.defaults.headers.common['X-CSRF-Token'] = token;
new Vue({
// ...应用配置
});
移动端适配
- 混合应用处理:
// 在 Cordova/Ionic 应用中
document.addEventListener('deviceready', () => {
const token = localStorage.getItem('csrf_token');
if (token) {
document.cookie = `csrf_token=${token}; path=/`;
}
});
- 原生应用集成:
// Android WebView 示例
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
// 注入 CSRF 令牌
String token = generateToken();
view.evaluateJavascript(
"document.cookie = 'csrf_token=" + token + "; path=/';",
null
);
}
});
日志与监控
- 攻击尝试记录:
app.post('/submit', (req, res, next) => {
const cookieToken = req.cookies.csrf_token;
const bodyToken = req.body.csrf_token;
if (!cookieToken || !bodyToken || cookieToken !== bodyToken) {
logCSRFAttempt(req);
return res.status(403).send('CSRF token验证失败');
}
next();
});
function logCSRFAttempt(req) {
const logEntry = {
timestamp: new Date(),
ip: req.ip,
userAgent: req.headers['user-agent'],
path: req.path,
cookieToken: req.cookies.csrf_token,
bodyToken: req.body.csrf_token
};
// 写入日志系统或安全监控平台
console.warn('CSRF尝试被阻止:', logEntry);
}
自动化测试
- 单元测试示例:
const request = require('supertest');
const app = require('../app');
describe('双重提交Cookie方案测试', () => {
let token;
test('获取表单时设置Cookie', async () => {
const res = await request(app).get('/form');
token = res.headers['set-cookie'][0].split(';')[0].split('=')[1];
expect(token).toHaveLength(32);
});
test('有效令牌通过验证', async () => {
const res = await request(app)
.post('/submit')
.set('Cookie', [`csrf_token=${token}`])
.send({ csrf_token: token });
expect(res.statusCode).toBe(200);
});
test('无效令牌被拒绝', async () => {
const res = await request(app)
.post('/submit')
.set('Cookie', ['csrf_token=wrong_token'])
.send({ csrf_token: 'wrong_token' });
expect(res.statusCode).toBe(403);
});
});
浏览器兼容性处理
- 旧版浏览器支持:
// 检测浏览器是否支持 SameSite Cookie
function supportsSameSite() {
try {
document.cookie = 'test=1; SameSite=Strict';
return document.cookie.includes('test=1');
} catch (e) {
return false;
}
}
// 根据支持情况调整 Cookie 设置
const cookieOptions = {
httpOnly: true,
secure: true
};
if (supportsSameSite()) {
cookieOptions.sameSite = 'strict';
}
res.cookie('csrf_token', token, cookieOptions);
安全头部增强
- Content Security Policy:
// 添加 CSP 头部
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
next();
});
- 其他安全头部:
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'same-origin');
next();
});
上一篇: SameSite Cookie 机制
下一篇: 前端框架中的 CSRF 防护