PHP CSRF Token 生成全攻略:从原理到实战,彻底告别跨站请求伪造
目录导读
- 什么是CSRF攻击?为什么需要Token?
- Token生成的核心原理:随机性与不可预测性
- PHP中生成CSRF Token的四种最佳实践(附代码)
- Token的存储与验证:Session vs Cookie陷阱
- 高级防护:双提交Cookie与SameSite整合
- 常见问题问答(FAQ)
- 构建无懈可击的防护层
什么是CSRF攻击?为什么需要Token?
想象一下,你登录了银行网站,然后在不退出登录的情况下访问了一个恶意网站,这个恶意网站偷偷向银行网站发送了一个“转账1000元”的请求,由于你的浏览器自动携带了银行的Cookie,服务器无法区分这个请求是你主动发起还是恶意网站伪造的——这就是跨站请求伪造(CSRF)。

核心痛点: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。
实现步骤:
- 服务端生成Token,写入
$_COOKIE['csrf_token'](使用setcookie)。 - 前端JS读取该Cookie值,放入
X-CSRF-Token请求头。 - 服务端比对请求头值与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(),而是加密安全随机数 + 会话绑定 + 严格验证的三位一体,最佳实践路线图:
- 优先使用Session存储,
random_bytes(32)生成。 - 使用
hash_equals()进行时间安全比较。 - 对敏感动作(如改密码、支付)额外使用双提交Cookie。
- 强制
SameSite=Strict或Lax。 - 定期更新框架(Laravel、Symfony已内置CSRF中间件)。
安全是设计出来的,不是测试出来的,将CSRF防护默认集成到每个表单和AJAX请求中,而不是事后补救,你可以用这些代码构建一道坚固的城墙,让攻击者无隙可乘。
(文章结束)