Token 机制防 CSRF:同步令牌模式的实现与踩坑

在 Web 安全领域,CSRF(跨站请求伪造)始终是 OWASP Top 10 的常客。它的核心逻辑并不复杂:攻击者诱导已登录的用户在不知情的情况下,向目标站点发起一个带有身份凭证的请求。浏览器会自动携带 Cookie,服务器无法区分这个请求是用户主动发起的,还是被第三方页面伪造的。防御 CSRF 的手段有很多,其中同步令牌模式(Synchronizer Token Pattern) 是最经典、也最可靠的方案之一。本文将从原理出发,给出可落地的实现代码,并重点梳理实际开发中容易踩的坑。

一、为什么 Cookie 防不住 CSRF

要理解同步令牌的价值,先要认清 Cookie 的“原罪”。浏览器遵循同源策略,但 <img><form><script> 等标签发起的请求并不受同源策略限制,而 Cookie 的发送只取决于目标域名,与请求来源页面无关。这意味着:

  • 用户在 bank.com 登录后,Cookie 被浏览器保存;
  • 用户访问了恶意站点 evil.com
  • evil.com 页面里放一个 <form action="https://bank.com/transfer" method="POST">,自动提交;
  • 浏览器带着 bank.com 的 Cookie 发出请求,银行服务器认为这是合法操作。

问题的根源在于:服务器仅凭 Cookie 无法验证请求是否来自本站页面。同步令牌的思路就是给每个请求附加一个攻击者无法获取的“暗号”。

二、同步令牌模式的核心原理

同步令牌模式包含三个关键点:

  1. 服务端生成随机令牌,与当前用户的会话绑定,存储在 Session 中;
  2. 页面渲染时把令牌嵌入表单或请求头,随请求一起提交;
  3. 服务端校验请求中的令牌与 Session 中的令牌是否一致,不一致则拒绝。

攻击者虽然能让浏览器自动带上 Cookie,但无法读取目标站点的页面内容(受同源策略保护),因此拿不到令牌。没有令牌,伪造的请求就会被服务端拒绝。

令牌需要满足两个条件:足够随机(不可预测)和与会话绑定(不可复用)。

三、实现示例

3.1 服务端生成与存储

以 PHP 为例,在用户会话建立时生成令牌:

session_start();

function generateCsrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

random_bytes 是密码学安全的随机源,不要用 rand()mt_rand(),它们可预测。

3.2 前端嵌入

在表单中放置隐藏字段:

<form action="/transfer" method="POST">
    <input type="hidden" name="csrf_token"
           value="<?= htmlspecialchars(generateCsrfToken()) ?>">
    <!-- 其他字段 -->
</form>

对于 AJAX 请求,可以把令牌放在自定义请求头中:

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(data)
});

3.3 服务端校验

function validateCsrfToken(): bool {
    $token = $_POST['csrf_token']
          ?? $_SERVER['HTTP_X_CSRF_TOKEN']
          ?? '';
    if (empty($token) || empty($_SESSION['csrf_token'])) {
        return false;
    }
    return hash_equals($_SESSION['csrf_token'], $token);
}

校验时必须使用 hash_equals 而非 ==,前者是恒定时间比较,能防止时序攻击。

四、踩坑实录

同步令牌看起来简单,但实际落地时坑不少,以下是最常见的几类。

坑一:只在 POST 上校验

很多开发者只对 POST 请求做校验,认为 GET 是“安全”的。但如果业务中存在 GET /delete?id=1 这类写操作,CSRF 照样成立。原则是:所有改变状态的请求都必须校验令牌,无论方法是什么。

坑二:令牌放在 URL 里

把令牌拼在 URL 上(如 ?csrf_token=xxx)会带来泄露风险:URL 可能被记录在浏览器历史、服务器访问日志、Referer 头中。令牌应放在请求体或自定义请求头里

坑三:多标签页导致令牌失效

如果每次渲染页面都重新生成令牌并覆盖 Session,用户在多个标签页操作时,旧标签页的令牌就会失效,提交时报错。正确做法是同一会话内保持令牌稳定,或者在令牌轮换时允许一个宽限窗口。

坑四:Session 与令牌的生命周期不同步

用户登录状态变化(登录、登出、权限提升)时,应当重新生成令牌,防止会话固定攻击。但要注意,登出后如果页面还缓存着旧令牌,再次提交会失败,需要前端配合刷新。

坑五:忽略 SameSite Cookie 的协同作用

现代浏览器支持 SameSite=Lax/Strict,能在很大程度上缓解 CSRF。但它不能完全替代令牌:老旧浏览器不支持,且 Lax 对顶级导航的 POST 仍有放行场景。建议令牌与 SameSite 双管齐下,而不是二选一。

坑六:API 与 SPA 场景下的误区

纯前后端分离的 SPA 如果使用 JWT 放在 Authorization 头中,天然不受 CSRF 影响,因为攻击者无法跨域读取或设置该头。但如果 JWT 存在 Cookie 里,就依然需要 CSRF 防护。判断标准只有一个:凭证是否由浏览器自动携带

坑七:令牌校验被绕过

常见疏漏包括:校验逻辑写在某个基类里但个别接口忘了调用;或者校验失败后仍然继续执行了业务逻辑。建议把校验做成中间件或统一入口,避免遗漏。

五、与其他防护的配合

同步令牌不是孤立的。一个完整的 Web 安全加固体系还应包括:

  • XSS 防护:XSS 能读取页面中的令牌,从而绕过 CSRF 防护。所以输出转义、CSP 策略必不可少;
  • SQL 注入防护:参数化查询,避免攻击者通过注入直接篡改数据;
  • 安全响应头SameSiteSecureHttpOnly 三件套;
  • 关键操作二次验证:转账、改密等敏感操作增加密码或验证码确认。

六、小结

同步令牌模式之所以经久不衰,是因为它用最简单的机制解决了最本质的问题:让服务器能够验证请求确实来自本站页面。实现上并不复杂,但细节决定成败——随机源要安全、比较要恒定时间、令牌要与会话绑定、所有写操作都要覆盖。避开上述几个坑,再配合 SameSite、XSS 防护等措施,就能构建起一道可靠的 CSRF 防线。安全没有银弹,但把每一个环节做对,攻击者的空间就会被压缩到最小。

未经允许不得转载:任鹏个人博客 » Token 机制防 CSRF:同步令牌模式的实现与踩坑

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏