您现在的位置是:网站首页 > 双重提交 Cookie 方案文章详情

双重提交 Cookie 方案

什么是双重提交 Cookie 方案

双重提交 Cookie 方案是一种用于防范跨站请求伪造(CSRF)攻击的安全机制。它的核心思想是让客户端在发送请求时携带两个相同的令牌:一个通过 Cookie 发送,另一个通过请求体或头部发送。服务器通过比较这两个令牌是否一致来验证请求的合法性。

工作原理

  1. 令牌生成:服务器在用户首次访问时生成一个随机令牌,并将其存储在用户的会话中
  2. Cookie 设置:服务器将该令牌通过 Set-Cookie 头部发送给客户端
  3. 表单嵌入:服务器在返回的 HTML 表单中包含一个隐藏字段,其值也是这个令牌
  4. 请求发送:客户端提交表单时,会自动携带 Cookie 中的令牌,同时也会提交表单中的令牌
  5. 服务器验证:服务器比较这两个令牌是否匹配,以此判断请求是否合法

实现示例

以下是使用 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 防护方法有几个优势:

  1. 无需服务器端存储:令牌只需在 Cookie 和表单中保持一致,不需要服务器维护状态
  2. 简单实现:不需要复杂的加密或签名机制
  3. 兼容性好:适用于各种前端框架和技术栈

但也要注意以下安全考虑:

  • Cookie 必须设置为 HttpOnly 和 Secure(如果使用 HTTPS)
  • 令牌必须有足够的随机性,防止被猜测
  • 对于敏感操作,建议结合其他安全措施

实际应用场景

  1. 传统表单提交
<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>
  1. 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')
  })
});

与其他方案的比较

  1. 同步令牌模式

    • 需要服务器存储令牌
    • 实现复杂度较高
    • 适用于所有类型的请求
  2. 双重提交 Cookie

    • 无需服务器存储
    • 实现简单
    • 依赖 Cookie 功能
  3. SameSite Cookie

    • 浏览器原生支持
    • 兼容性问题
    • 不能完全替代 CSRF 防护

最佳实践建议

  1. 令牌生成
// 使用加密安全的随机数生成器
function generateStrongToken() {
  return crypto.randomBytes(32).toString('hex');
}
  1. Cookie 设置
res.cookie('csrf_token', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000 // 1天
});
  1. 多页面应用处理
// 在全局 JavaScript 中设置 AJAX 默认头部
$.ajaxSetup({
  beforeSend: function(xhr) {
    xhr.setRequestHeader('X-CSRF-Token', getCookie('csrf_token'));
  }
});

常见问题解决

  1. Cookie 被禁用

    • 提供降级方案
    • 显示错误提示
    • 考虑使用 localStorage 替代方案
  2. 多标签页问题

    • 每个页面生成独立令牌
    • 或者使用持久化的主令牌
  3. 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();
}

性能考虑

  1. 令牌生成开销

    • 对于高并发应用,考虑预生成令牌池
    • 使用性能更好的随机数生成器
  2. 验证逻辑优化

// 快速失败验证
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)
  );
}

框架集成示例

  1. 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>
  );
}
  1. 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({
  // ...应用配置
});

移动端适配

  1. 混合应用处理
// 在 Cordova/Ionic 应用中
document.addEventListener('deviceready', () => {
  const token = localStorage.getItem('csrf_token');
  if (token) {
    document.cookie = `csrf_token=${token}; path=/`;
  }
});
  1. 原生应用集成
// 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
    );
  }
});

日志与监控

  1. 攻击尝试记录
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);
}

自动化测试

  1. 单元测试示例
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);
  });
});

浏览器兼容性处理

  1. 旧版浏览器支持
// 检测浏览器是否支持 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);

安全头部增强

  1. 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();
});
  1. 其他安全头部
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();
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步