阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 双重提交 Cookie 方案

双重提交 Cookie 方案

作者:陈川 阅读数:19124人阅读 分类: 前端安全

什么是双重提交 Cookie 方案

双重提交 Cookie 方案是一种防御 CSRF(跨站请求伪造)攻击的技术手段。它的核心思想是让客户端在发送请求时携带两个相同的令牌:一个通过 Cookie 自动发送,另一个通过表单字段或 HTTP 头显式发送。服务器通过比较这两个值是否匹配来验证请求的合法性。

这种方案之所以有效,是因为攻击者虽然能利用用户的 Cookie(由于同源策略),但无法读取 Cookie 内容或在前端代码中设置自定义 HTTP 头。当服务器要求两个令牌必须匹配时,伪造的请求就会因为缺少其中一个令牌而被拒绝。

工作原理详解

  1. 令牌生成:服务器在用户会话开始时生成一个随机加密令牌
  2. 令牌分发
    • 通过 Set-Cookie 头将令牌写入 Cookie
    • 同时将相同令牌嵌入页面(如 meta 标签或 JavaScript 变量)
  3. 请求验证
    • 浏览器自动携带 Cookie 中的令牌
    • 前端代码显式携带另一个令牌(表单字段/HTTP 头)
  4. 服务器比对:验证两个令牌是否匹配且未过期
// 服务器端生成令牌示例(Node.js)
const crypto = require('crypto');

function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

// Express 中间件示例
app.use((req, res, next) => {
  if (!req.cookies._csrf) {
    const token = generateCSRFToken();
    res.cookie('_csrf', token, { 
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production'
    });
    res.locals.csrfToken = token; // 供模板使用
  }
  next();
});

具体实现方式

表单提交场景

对于传统表单提交,通常将 CSRF 令牌作为隐藏字段插入:

<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <input type="text" name="amount">
  <button type="submit">转账</button>
</form>

AJAX 请求场景

现代前端应用通常需要适配 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 请求添加 CSRF 头
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-TOKEN': getCookie('_csrf')
  },
  body: JSON.stringify({ amount: 1000 })
});

同源策略的影响

双重提交方案依赖于浏览器的同源策略:

  • Cookie 随请求自动发送(受 SameSite 属性影响)
  • 攻击者无法读取目标站点的 Cookie 内容
  • 自定义 HTTP 头无法被跨域请求携带

安全增强措施

SameSite Cookie 属性

结合 SameSite 属性可以进一步增强安全性:

res.cookie('_csrf', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // 或 'lax'
});

令牌绑定

将令牌与用户会话绑定可防止令牌替换攻击:

// 验证时不仅比较值,还要验证令牌归属
function verifyCSRFToken(req) {
  const cookieToken = req.cookies._csrf;
  const bodyToken = req.body._csrf || req.headers['x-csrftoken'];
  
  if (!cookieToken || !bodyToken) return false;
  if (cookieToken !== bodyToken) return false;
  
  // 可选:验证令牌是否属于当前会话
  return validateTokenInSession(req.session.userId, cookieToken);
}

常见问题与解决方案

多标签页场景

当用户打开多个标签页时,可能会遇到令牌不一致问题。解决方案包括:

  • 使用会话级 Cookie 而非持久化 Cookie
  • 为每个表单生成唯一令牌并维护令牌池
// 令牌池实现示例
const tokenPool = new Map();

app.post('/submit', (req, res) => {
  const { _csrf } = req.body;
  if (!tokenPool.get(req.sessionID)?.includes(_csrf)) {
    return res.status(403).send('Invalid CSRF token');
  }
  // 验证通过后移除已用令牌
  tokenPool.set(req.sessionID, 
    tokenPool.get(req.sessionID).filter(t => t !== _csrf));
});

API 兼容性问题

对于需要同时支持 web 和 native app 的场景,可以采用:

  • 为 web 保留双重提交机制
  • 为 native app 使用基于签名的认证方案
// 条件性 CSRF 检查中间件
app.use((req, res, next) => {
  if (req.path.startsWith('/api/') && 
      req.headers['x-requested-with'] === 'mobile-app') {
    return next(); // 跳过 CSRF 检查
  }
  // 正常 CSRF 验证流程
});

性能优化实践

令牌缓存策略

对于高流量场景,可以采用以下优化:

  • 使用 HMAC 签名减少服务器端存储
  • 设置合理的令牌过期时间
// HMAC 签名示例
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(userSessionId);
const token = hmac.digest('hex');

// 验证时只需重新计算比对
function verifyToken(sessionId, token) {
  const hmac = crypto.createHmac('sha256', SECRET_KEY);
  hmac.update(sessionId);
  return hmac.digest('hex') === token;
}

CDN 场景下的适配

当静态资源托管在 CDN 时,需要特殊处理:

<!-- 使用 meta 标签传递令牌 -->
<meta name="csrf-token" content="{{csrfToken}}">

<script>
// 前端统一读取逻辑
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
</script>

与其他安全机制的对比

与传统 CSRF Token 对比

特性 传统方案 双重提交方案
令牌存储位置 Session/DB Cookie
前端参与度
分布式支持 需要共享存储 无状态
防御力度

与加密令牌方案对比

加密令牌(如 JWT)虽然也可以用于 CSRF 防护,但存在不同特性:

  • 加密令牌可以包含更多信息
  • 但无法实现即时吊销
  • 需要处理密钥轮换问题
// JWT 实现示例
const jwt = require('jsonwebtoken');

function generateToken(userId) {
  return jwt.sign({ userId }, SECRET_KEY, { expiresIn: '1h' });
}

function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET_KEY);
  } catch {
    return null;
  }
}

实际部署注意事项

负载均衡环境

在多服务器环境下需要确保:

  • 所有服务器使用相同的密钥生成令牌
  • 时钟同步对于时间敏感型令牌很重要
// 使用共享配置的密钥
const SECRET_KEY = process.env.CSRF_SECRET || 'default-secret';

// 或者从中央配置服务获取
async function getCSRFSecret() {
  return fetchConfigService('csrf_secret');
}

日志与监控

建议记录 CSRF 验证失败事件用于安全分析:

app.post('/submit', (req, res) => {
  if (!verifyCSRFToken(req)) {
    securityLogger.warn('CSRF validation failed', {
      ip: req.ip,
      path: req.path,
      userAgent: req.get('User-Agent')
    });
    return res.status(403).send('Forbidden');
  }
  // 正常处理逻辑
});

浏览器兼容性处理

旧版浏览器回退方案

对于不支持 SameSite 属性的浏览器,可以:

  • 增加额外的验证参数
  • 使用 postMessage 进行令牌同步
// 检测 SameSite 支持
const isSameSiteSupported = () => {
  try {
    document.cookie = 'test=1; SameSite=Lax';
    return document.cookie.includes('test=1');
  } catch {
    return false;
  }
};

if (!isSameSiteSupported()) {
  // 启用传统验证模式
  window.addEventListener('message', syncCSRFToken);
}

移动端特殊处理

某些移动浏览器对 Cookie 的处理存在差异,建议:

  • 增加 UA 检测逻辑
  • 为移动端使用更长的令牌有效期
function isMobileBrowser(ua) {
  return /Mobile|Android|iP(hone|od|ad)/.test(ua);
}

app.use((req, res, next) => {
  const isMobile = isMobileBrowser(req.get('User-Agent'));
  res.cookie('_csrf', token, {
    httpOnly: true,
    secure: true,
    maxAge: isMobile ? 86400000 : 1800000 // 移动端1天,web端30分钟
  });
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌