为什么我的CSRF令牌验证总是失败?

东方樱潼 阅读 29

我在表单提交里用了同步令牌防护,前端生成token存到cookie和隐藏字段,但提交后后端一直报错说令牌无效,这是哪里出错了呢?

我的实现是这样的:每次页面加载时用JavaScript生成随机字符串存到cookie和表单里,提交时通过AJAX把cookie里的token和表单里的token一起发送。但后端验证时两个值总对不上:


// 生成令牌的代码
function generateToken() {
  const token = Math.random().toString(36).substr(2, 10);
  document.cookie = `csrfToken=${token}; path=/`;
  document.getElementById('csrfField').value = token;
}

// 表单提交时的AJAX
document.querySelector('form').addEventListener('submit', (e) => {
  e.preventDefault();
  const formToken = new FormData(e.target).get('csrfToken');
  fetch('/api/submit', {
    method: 'POST',
    headers: { 'X-CSRF-Token': document.cookie }, // 这里有问题吗?
    body: JSON.stringify({ ...formValues, csrfField: formToken })
  });
});

后端PHP验证代码是直接对比$_COOKIE[‘csrfToken’]和POST[‘csrfField’],但每次都是不匹配。明明前端同时设置了cookie和表单字段,为什么会出现这种不一致?

我来解答 赞 6 收藏
二维码
手机扫码查看
1 条解答
欧阳春红
你的问题确实是一个常见的CSRF令牌验证失败的情况。咱们一步一步来分析,看看问题出在哪里,并且给出一个可行的解决方案。

### 1. 先说说为什么会不一致
你在前端生成了一个随机字符串,同时存到了 cookie 和表单隐藏字段中。但问题是,document.cookie 这个东西有点复杂。当你设置 document.cookie = 'csrfToken=xxx' 的时候,浏览器会自动管理这个 cookie 的生命周期,包括路径、域名等信息。如果你没有显式指定这些属性,可能会导致一些意外行为。

另外,AJAX 请求中的 headers 部分,你直接用了 document.cookie,但实际上 document.cookie 返回的是 **所有** 当前域名下的 cookie 字符串(格式是 key1=value1; key2=value2),而不是单个 csrfToken 的值。这就导致后端接收到的 X-CSRF-Token 头部可能包含了其他无关的 cookie 数据,而不是你想要的那个 token。

### 2. 解决方案
我们需要确保以下几点:
- 前端生成的 token 能正确地传递到后端。
- 后端能够正确解析并验证这个 token。

#### 第一步:改进前端代码
我们先来看一下如何更可靠地从 cookie 中提取出具体的 token 值。可以写一个简单的函数来获取指定名称的 cookie 值:

// 获取指定名称的 cookie 值
function getCookie(name) {
const value = ; ${document.cookie};
const parts = value.split(; ${name}=);
if (parts.length === 2) return parts.pop().split(';').shift();
}

// 生成令牌的代码保持不变
function generateToken() {
const token = Math.random().toString(36).substr(2, 10);
document.cookie = csrfToken=${token}; path=/;
document.getElementById('csrfField').value = token;
}

// 表单提交时的 AJAX
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault();
const formToken = new FormData(e.target).get('csrfToken');
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 注意这里要加 Content-Type
'X-CSRF-Token': getCookie('csrfToken') // 确保只传递 csrfToken 的值
},
body: JSON.stringify({ ...formValues, csrfField: formToken })
});
});


重点在于 getCookie 函数,它能帮你从 document.cookie 中准确提取出 csrfToken 的值,而不是把所有的 cookie 都扔过去。

#### 第二步:改进后端验证逻辑
后端 PHP 需要验证两个地方的 token 是否一致:一个是请求头中的 X-CSRF-Token,另一个是 POST 数据中的 csrfField。我们可以稍微调整一下验证逻辑:

<?php

// 获取请求头中的 CSRF Token
$csrfHeader = '';
if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'];
}

// 获取 POST 数据中的 CSRF Token
$csrfPost = isset($_POST['csrfField']) ? $_POST['csrfField'] : '';

// 比较两者是否一致
if ($csrfHeader === $csrfPost) {
echo "CSRF Token 验证成功";
} else {
echo "CSRF Token 不匹配";
}


这样写的好处是,我们明确指定了从哪里获取 token,并且用严格相等 (===) 来比较它们是否完全一致。

### 3. 原理解释
为什么之前会出问题?主要是因为:
- document.cookie 返回的是所有 cookie 的字符串,而不是单独的一个值。
- 如果请求头中的 X-CSRF-Token 包含了多余的 cookie 数据,后端在解析时就会出现问题。
- 同时,如果 cookie 的路径或域名设置不对,也可能导致前端读取不到正确的 token。

通过上面的改进,我们确保了:
1. 前端正确提取并发送了 csrfToken
2. 后端能够准确验证这两个值。

### 4. 小贴士
最后再提醒一下,CSRF 保护虽然很重要,但也别忘了结合 HTTPS 使用,否则 token 在传输过程中可能会被截获。另外,生成 token 的方式也可以考虑使用更强的随机数生成方法,比如 crypto.getRandomValues(),这样安全性会更高一些。

希望这些解释能帮到你!如果有其他问题随时问我。
点赞 1
2026-01-31 14:13