CSRF(跨站请求伪造)是网站安全中最常见也最容易被忽视的攻击方式之一。攻击者利用用户已登录的身份,诱导浏览器向目标网站发送非用户本意的请求,从而完成转账、修改密码、删除数据等恶意操作。防御CSRF的核心手段有两个:一是在每个表单或请求中嵌入随机生成的CSRF令牌(Token),服务端验证令牌的合法性;二是通过同源策略(Same-Origin Policy)配置,限制跨域请求的来源。这两项措施必须配合使用,才能构建起可靠的防护体系。下面我会从令牌生成机制、服务端验证逻辑、同源检查配置、以及实际部署中的注意事项,逐一拆解。

一、CSRF令牌的生成原理与最佳实践

CSRF令牌本质上是一段不可预测的随机字符串,它与用户会话绑定,每次请求都需要携带这个令牌。生成令牌时,必须满足三个条件:足够长(至少32字节)、足够随机(使用密码学安全的随机数生成器)、与用户会话唯一关联。很多开发者用简单的MD5或者时间戳拼接来生成令牌,这种做法在现代攻击面前已经不够安全了。

在Node.js环境下,推荐使用crypto模块的randomBytes方法生成令牌:

const crypto = require('crypto');

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

// 将令牌存入session
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateCSRFToken();
  }
  next();
});

在Java Spring框架中,可以通过内置的CsrfTokenRepository来自动生成和管理令牌,开发者只需要在模板中渲染令牌即可。在Python Flask或Django中,也有对应的中间件自动处理令牌的生成与校验。关键是不要自己造轮子,优先使用框架提供的成熟方案。

二、令牌的传递方式与前端嵌入

令牌生成后,需要在每次请求时传递给服务端。常见的传递方式有三种:隐藏表单字段、自定义请求头、以及Cookie同步模式。对于传统的表单提交(POST表单),最简单的方式是在HTML表单中加入一个隐藏字段:

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

对于AJAX请求,推荐通过自定义请求头传递令牌,例如使用X-CSRF-Token头部。前端代码示例如下:

fetch('/api/update-profile', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
  },
  body: JSON.stringify({ name: 'new_name' })
});

这里有一个细节很多人会忽略:令牌应该同时放在页面的meta标签中,方便JavaScript统一读取。在模板引擎中渲染页面时,把令牌写入meta标签是一个标准化的做法。

三、服务端验证逻辑的实现要点

服务端收到请求后,必须从请求体或请求头中提取令牌,并与session中存储的令牌进行比对。比对时要使用恒定时间比较函数(constant-time comparison),防止时序攻击(timing attack)。如果令牌不匹配或缺失,直接返回403 Forbidden状态码,不要返回200。

以下是一个Express中间件的验证示例:

const crypto = require('crypto');

function csrfProtection(req, res, next) {
  const tokenFromHeader = req.headers['x-csrf-token'];
  const tokenFromBody = req.body._csrf;
  const sessionToken = req.session.csrfToken;

  const clientToken = tokenFromHeader || tokenFromBody;

  if (!clientToken || !sessionToken) {
    return res.status(403).json({ error: 'CSRF token missing' });
  }

  // 使用恒定时间比较
  if (!crypto.timingSafeEqual(
    Buffer.from(clientToken),
    Buffer.from(sessionToken)
  )) {
    return res.status(403).json({ error: 'CSRF token invalid' });
  }

  next();
}

需要特别注意的是,GET请求不应该触发CSRF防护逻辑,因为GET请求本身不应该修改数据。但如果你的系统中存在用GET做修改操作的情况(这本身就是设计缺陷),那就需要对所有请求都做校验。另外,令牌在用户每次登录或会话刷新时应该重新生成,避免令牌被长期复用带来的风险。

四、同源检查(Same-Origin Policy)的配置策略

同源策略是浏览器的核心安全机制,它规定了一个页面只能向与自身同源(协议+域名+端口完全一致)的地址发起请求。但在实际开发中,我们经常需要跨域通信,这时候就需要通过CORS(跨域资源共享)头来显式控制哪些来源可以访问资源。正确配置CORS头是同源检查的关键环节。

在Nginx中配置CORS的基本方式如下:

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # 明确指定允许的来源,不要使用通配符 *
        add_header 'Access-Control-Allow-Origin' 'https://www.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, X-CSRF-Token';
        add_header 'Access-Control-Allow-Credentials' 'true';

        # 预检请求缓存
        add_header 'Access-Control-Max-Age' 3600;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend;
    }
}

这里有几个关键点:第一,Access-Control-Allow-Origin不要设置为*,尤其是在需要携带Cookie(凭证)的场景下,浏览器会直接拒绝。第二,Access-Control-Allow-Headers必须包含你自定义的CSRF令牌头部名称。第三,预检请求(OPTIONS)要单独处理并快速返回,避免浪费服务器资源。

五、双重Cookie防御模式的补充说明

除了令牌验证和CORS配置,还有一种被称为"双重Cookie"的防御模式。其原理是:服务端在响应中设置一个Cookie,同时要求前端在请求时从Cookie中读取值并放入请求头或请求体中。由于攻击者无法读取目标域的Cookie(同源策略限制),所以无法伪造这个值。这种方式可以作为令牌机制的补充,但不能完全替代令牌验证。

// 服务端设置双重Cookie
res.cookie('csrf_cookie', csrfToken, {
  httpOnly: false,  // 必须设为false,让JS能读取
  sameSite: 'Strict',
  secure: true
});

// 前端读取并附加到请求
const csrfCookie = document.cookie
  .split('; ')
  .find(row => row.startsWith('csrf_cookie='))
  .split('=')[1];

fetch('/api/action', {
  headers: { 'X-CSRF-Token': csrfCookie }
});

需要注意的是,sameSite属性设置为Strict或Lax可以进一步防止Cookie在跨站场景下被发送。但如果你的业务需要跨站携带Cookie,就必须配合CORS和令牌机制一起使用。

六、实际部署中容易踩的坑

第一个坑是令牌泄露。如果页面中存在XSS漏洞,攻击者可以通过JavaScript读取页面上的CSRF令牌,那么令牌机制就形同虚设。所以CSRF防护必须和XSS防护同步进行,内容安全策略(CSP)头也要正确配置。

第二个坑是忽略了API接口的防护。很多团队只在传统表单页面做了CSRF防护,但忘记了RESTful API接口同样需要防护。尤其是使用JSON格式提交的接口,必须通过请求头传递令牌。

第三个坑是令牌没有与会话绑定。如果令牌是全局唯一的而不是与用户会话绑定,那么一个用户的令牌可能被另一个用户利用。每个用户的会话必须有独立的令牌。

第四个坑是缓存问题。如果页面被缓存(CDN或浏览器缓存),缓存的页面中包含的令牌可能是过期的或属于其他用户的。对于包含CSRF令牌的页面,必须设置Cache-Control: no-store或no-cache。

七、总结与行动建议

CSRF防护不是单一措施能解决的问题,它需要令牌生成、令牌传递、服务端验证、同源策略配置、Cookie安全属性、以及XSS防护等多层机制协同工作。对于任何一个生产环境的网站,我的建议是:优先使用成熟框架自带的CSRF中间件,不要自己从零实现;同时在Web服务器层面正确配置CORS头和安全响应头;定期进行安全扫描和渗透测试,验证防护措施的有效性。安全不是一次性的工作,而是持续迭代的过程。