本文目录导读:

在PHP项目中防止CSRF(跨站请求伪造)攻击,最核心、最有效的做法是使用CSRF Token,结合其他辅助手段可以构建更坚固的防御体系。
以下是具体的实现方案和最佳实践:
核心方案:CSRF Token
这是对抗CSRF的行业标准方案,核心思想是:服务器生成一个随机且唯一的Token,嵌入到表单或请求中;当用户提交请求时,服务器验证该Token是否合法。
生成和存储Token
在用户会话(Session)中生成并存储一个安全的随机Token。
<?php
session_start();
// 生成CSRF Token(如果不存在)
if (empty($_SESSION['csrf_token'])) {
// 使用 random_bytes 生成更安全的随机数
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
在表单中嵌入Token
在HTML表单中添加一个隐藏字段,值为Session中的Token。
<form method="POST" action="/process.php">
<!-- 核心:隐藏的CSRF Token -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<!-- 其他表单字段 -->
<input type="text" name="username" required>
<input type="submit" value="提交">
</form>
重要: 使用 htmlspecialchars() 防止XSS攻击导致Token泄露。
验证Token
在处理POST、PUT、DELETE等有副作用的请求时,验证提交的Token是否与Session中的Token一致。
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证Token存在且匹配
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// Token不匹配,请求无效
die('CSRF验证失败,请求被拒绝。');
}
// Token验证通过,继续处理业务逻辑
// ... 处理表单数据 ...
}
?>
进阶与辅助方案
使用一次性Token(Token+Double Submit Cookie)
对于更敏感的操作(如支付、账户删除),建议使用一次性Token,即验证成功后立即销毁旧Token并生成新Token,这可以防止Token被重放。
// 验证成功后
if ($_POST['csrf_token'] === $_SESSION['csrf_token']) {
// 处理业务...
// 立即生成新Token,使旧Token失效
unset($_SESSION['csrf_token']);
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
检查HTTP Referer / Origin
作为第二道防线,可以检查请求头中的 Origin 或 Referer 字段,确认请求来源于本网站。
$allowed_origins = ['https://www.example.com', 'https://admin.example.com'];
if (isset($_SERVER['HTTP_ORIGIN'])) {
if (!in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
die('非法来源的请求。');
}
}
注意: 不能完全依赖此方法,因为 Referer 可能被浏览器禁用或伪造。
AJAX请求的Token传递
对于SPA或AJAX请求,通常将Token放在请求头中。
前端(jQuery示例):
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
后端(PHP):
// 从请求头中获取Token
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if ($token !== $_SESSION['csrf_token']) {
die('CSRF验证失败。');
}
HTML中设置Meta标签:
<meta name="csrf-token" content="<?php echo $_SESSION['csrf_token']; ?>">
需要避免的做法
- 不要使用GET请求执行有副作用的操作(如删除、修改),GET请求容易被嵌入图片、链接中。
- 不要使用简单的、可预测的Token生成方式(如
md5(uniqid())),应使用random_bytes()。 - 不要将Token通过URL传递,容易在Referer中泄露。
企业级防御清单
| 防御措施 | 重要性 | 说明 |
|---|---|---|
| CSRF Token | 必需 | 核心方案,每个用户会话唯一,不可预测。 |
| 一次性Token | 强烈推荐 | 用于敏感操作,阻止Token重放。 |
| SameSite Cookie | 重要 | 设置Cookie的 SameSite 属性为 Strict 或 Lax,现代浏览器限制跨站请求携带Cookie。 |
| Referer/Origin检查 | 辅助 | 增加一道验证,但不可作为唯一手段。 |
| 验证码 | 可选 | 对高风险操作(如大额转账)可加入验证码。 |
| HTTP方法限制 | 基础 | 只允许正确的HTTP方法(如POST)处理写操作。 |
完整示例(简易框架风格)
安全类(Security.php)
class CSRF {
public static function generateToken() {
if (!isset($_SESSION)) session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function getTokenField() {
$token = self::generateToken();
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
}
public static function validateToken($token) {
if (!isset($_SESSION)) session_start();
if (!isset($_SESSION['csrf_token'])) return false;
return hash_equals($_SESSION['csrf_token'], $token); // 使用hash_equals防时序攻击
}
}
视图中使用(form.php)
<form method="POST">
<?php echo CSRF::getTokenField(); ?>
<input type="text" name="data">
<button type="submit">提交</button>
</form>
控制器中验证(process.php)
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CSRF::validateToken($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('CSRF验证失败');
}
// 业务逻辑...
}
通过以上方案,你可以有效地防御绝大多数CSRF攻击。没有银弹,但结合Token、SameSite Cookie、Referer检查和正确的HTTP方法,你的PHP项目将具备非常高的安全性。