PHP CSRF Token 怎么生成

wen PHP项目 1

PHP CSRF Token 生成全攻略:从原理到实战,彻底告别跨站请求伪造


目录导读

  1. 什么是CSRF攻击?为什么需要Token?
  2. Token生成的核心原理:随机性与不可预测性
  3. PHP中生成CSRF Token的四种最佳实践(附代码)
  4. Token的存储与验证:Session vs Cookie陷阱
  5. 高级防护:双提交Cookie与SameSite整合
  6. 常见问题问答(FAQ)
  7. 构建无懈可击的防护层

什么是CSRF攻击?为什么需要Token?

想象一下,你登录了银行网站,然后在不退出登录的情况下访问了一个恶意网站,这个恶意网站偷偷向银行网站发送了一个“转账1000元”的请求,由于你的浏览器自动携带了银行的Cookie,服务器无法区分这个请求是你主动发起还是恶意网站伪造的——这就是跨站请求伪造(CSRF)。

PHP CSRF Token 怎么生成

核心痛点:Cookie是自动附加的,服务器无法验证请求的“主观意图”,需要一种服务器与客户端共享的、随机的、一次性或短时效的密钥来验证请求来源的合法性,这个密钥就是CSRF Token。


Token生成的核心原理:随机性与不可预测性

Token必须满足两个条件:

  • 不可猜测:必须使用密码学安全的伪随机数生成器(CSPRNG),而非rand()mt_rand()
  • 绑定会话:Token必须存储在Session中,且与当前用户唯一绑定。

错误示范

// 危险:可预测的随机数
$token = md5(time() . rand(0, 999));

正确示范:使用PHP 7.0+内置的random_bytes()bin2hex()


PHP中生成CSRF Token的四种最佳实践(附代码)

标准Session存储(最常用)

// 生成或复用Token
function getCsrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}
// 在表单中输出
echo '<input type="hidden" name="csrf_token" value="' . getCsrfToken() . '">';
// 验证时
function verifyCsrfToken($submittedToken) {
    return hash_equals($_SESSION['csrf_token'], $submittedToken);
}

每次请求轮换Token

// 登录成功后或每次表单提交后重新生成
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

优点:可防止固定Token被长期窃取。缺点:多标签页操作会冲突。

基于HMAC的Token(无需Session)

function generateToken($userId) {
    $secret = '你的应用密钥'; // 存储在配置文件中
    return hash_hmac('sha256', $userId . session_id(), $secret);
}
// 注意:仍需在Session中存储session_id,或通过Cookie传递

双提交Cookie模式(无Session)

// 生成并写入Cookie(JS可读)
setcookie('csrf_cookie', bin2hex(random_bytes(32)), time()+3600, '/', '', true, true);
// 前端从Cookie读取该值并放入请求头或表单字段
// 后端比较请求头中的token与Cookie中的token是否一致

Token的存储与验证:Session vs Cookie陷阱

误区

  • ❌ 将Token放在$_GET中(会泄漏到日志和浏览器历史)。
  • ❌ 用$_COOKIE直接存储Token(易被XSS读取)。
  • ❌ 验证时用比较(存在时序攻击风险,必须用hash_equals())。

推荐做法

  • 存储$_SESSION是最安全的选择(默认HttpOnly,JS无法读取)。
  • 验证时机:对所有POST、PUT、DELETE请求进行校验,白名单除外(如支付回调)。

代码模板

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        http_response_code(403);
        die('CSRF验证失败,请刷新页面重试');
    }
}

高级防护:双提交Cookie与SameSite整合

场景:当使用API或前后端分离时,Session可能无法正常工作,此时可采用自定义请求头 + 双提交Cookie

实现步骤

  1. 服务端生成Token,写入$_COOKIE['csrf_token'](使用setcookie)。
  2. 前端JS读取该Cookie值,放入X-CSRF-Token请求头。
  3. 服务端比对请求头值与Cookie值是否一致。

配合SameSite属性

setcookie('csrf_cookie', $token, [
    'expires' => time()+3600,
    'samesite' => 'Strict', // 或Lax
    'secure' => true,
    'httponly' => true
]);

说明SameSite=Strict可拦截所有跨站Cookie发送,是最有力的补充,但会牺牲部分跨站跳转体验,建议使用Lax并配合Token双重校验。


常见问题问答(FAQ)

Q1:Token能保持多久? 建议设置15-30分钟过期,并记录生成时间戳,过期后要求重新登录或刷新表单。

Q2:如果用户开了多个标签页怎么办? 采用每次请求后不立即销毁Token,而是允许在同一个Session生命周期内复用,但每次登录后重新生成,或者使用Token池(存储最近5个有效Token)。

Q3:Token会被XSS窃取吗? 如果Token放在Session中且Session Cookie设置了HttpOnly,XSS无法直接读取,但如果存在DOM型XSS可通过AJAX请求伪造表单提交,因此必须同时做好XSS过滤。

Q4:API接口是否需要CSRF Token? 需要,对于无状态API,使用Bearer Token(如JWT)+ 自定义头可有效防CSRF,因为跨站请求无法自定义头。

Q5:如何测试CSRF防护是否有效? 使用Burp Suite或OWASP ZAP,拦截正常请求,修改Token为随机值,看是否返回403,同时尝试删除Token字段。


构建无懈可击的防护层

生成CSRF Token不是简单的rand(),而是加密安全随机数 + 会话绑定 + 严格验证的三位一体,最佳实践路线图:

  1. 优先使用Session存储random_bytes(32)生成。
  2. 使用hash_equals()进行时间安全比较
  3. 对敏感动作(如改密码、支付)额外使用双提交Cookie
  4. 强制SameSite=StrictLax
  5. 定期更新框架(Laravel、Symfony已内置CSRF中间件)

安全是设计出来的,不是测试出来的,将CSRF防护默认集成到每个表单和AJAX请求中,而不是事后补救,你可以用这些代码构建一道坚固的城墙,让攻击者无隙可乘。


(文章结束)

抱歉,评论功能暂时关闭!