本文目录导读:

在 PHP 项目中添加 CSRF 令牌防护,通常遵循以下标准流程:
生成 CSRF 令牌(通常在 Session 中)
首先要有一个安全的令牌生成函数:
// 初始化 session
session_start();
// 生成 CSRF 令牌
function generateCsrfToken() {
// session 中没有令牌,则生成一个新的
if (empty($_SESSION['csrf_token'])) {
// 使用 random_bytes 生成安全的随机值
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 获取当前令牌
$csrfToken = generateCsrfToken();
在表单中添加隐藏字段
将令牌嵌入到所有需要保护的 POST/PUT/DELETE 表单中:
<form method="POST" action="/submit.php">
<!-- 其他表单字段 -->
<input type="text" name="username">
<input type="password" name="password">
<!-- CSRF 令牌隐藏字段 -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit">提交</button>
</form>
对于 AJAX 请求,可以在 HTML 的 meta 标签中存储令牌:
<meta name="csrf-token" content="<?php echo htmlspecialchars($csrfToken); ?>">
然后在 JavaScript 中读取并发送:
// 使用 Fetch API
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({/* 请求体 */})
});
// 或者在 AJAX 中
$.ajax({
url: '/api/endpoint',
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken
},
data: { /* 数据 */ }
});
验证 CSRF 令牌(在服务端处理请求时)
在每个需要保护的后端处理入口进行验证:
// 验证 CSRF 令牌的函数
function validateCsrfToken($token) {
// 检查 session 中是否存在令牌
if (!isset($_SESSION['csrf_token'])) {
return false;
}
// 使用 hash_equals 防止时序攻击
return hash_equals($_SESSION['csrf_token'], $token);
}
// 在 POST 请求处理中
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 获取提交的令牌
$submittedToken = $_POST['csrf_token'] ?? '';
// 验证令牌
if (!validateCsrfToken($submittedToken)) {
// 令牌无效,终止请求
http_response_code(403);
die('CSRF 令牌验证失败,请求被拒绝。');
}
// 令牌验证通过,继续处理请求
// ... 业务逻辑代码
}
可选:令牌轮换(提高安全性)
每次成功验证后可以生成新令牌(防止令牌被反复使用):
function rotateCsrfToken() {
// 生成新令牌并覆盖旧令牌
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
return $_SESSION['csrf_token'];
}
完整封装示例
创建一个 CSRF 防护类以便复用:
class CsrfProtection {
public static function init() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public static function getToken() {
self::init();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function validateToken($token) {
self::init();
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
public static function generateTokenField() {
$token = self::getToken();
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
}
}
使用方式:
// 页面模板中
<!DOCTYPE html>
<html>
<body>
<form method="POST" action="/process.php">
<?php echo CsrfProtection::generateTokenField(); ?>
<input type="text" name="data">
<input type="submit">
</form>
</body>
</html>
// 后端处理中
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!CsrfProtection::validateToken($token)) {
die('CSRF 防护触发');
}
// 处理数据...
}
重要注意事项
- 永远不要使用 GET 请求修改服务器状态(增删改操作必须使用 POST/PUT/DELETE)
- 令牌必须使用安全的随机生成器(
random_bytes或openssl_random_pseudo_bytes) - 比较令牌时一定要使用
hash_equals防止时序攻击 - 令牌应该绑定到用户 session(不同用户不同令牌)
- 对于高安全需求,考虑添加 Double Submit Cookie 模式(令牌同时放在 Cookie 和请求体中,服务端对比两者是否一致)
这样基本就能有效防御 CSRF 攻击了。