csrf即跨站请求伪造,攻击者会构造恶意页面,诱导已登录目标网站的用户点击,利用用户的登录态发起非预期请求,比如修改密码、转账等操作。php应用中防御csrf攻击最常用的方式就是生成验证token,在合法请求中携带token,服务端校验通过才处理请求。

csrf攻击的基本原理
用户登录站点A后,浏览器会保存站点A的登录凭证比如session_id。此时如果用户访问恶意站点B,站点B的页面中构造了指向站点A的恶意请求,比如一个隐藏的表单提交或者图片请求,浏览器发起这个请求时会自动带上站点A的cookie,站点A的服务端无法区分这个请求是用户主动发起还是被恶意诱导的,就会执行对应的操作,这就是csrf攻击的核心逻辑。
php生成csrf token的实现
生成token需要满足随机性、唯一性、不可预测性,通常可以结合随机字符串和加密算法实现,以下是生成token的示例代码:
<?php
session_start();
/**
* 生成csrf token
* @return string 生成的token字符串
*/
function generate_csrf_token() {
// 生成32位随机字符串作为token原始内容
$random_str = bin2hex(random_bytes(16));
// 存储token到session,用于后续校验
$_SESSION['csrf_token'] = $random_str;
// 设置token过期时间,这里设置为30分钟后过期
$_SESSION['csrf_token_expire'] = time() + 1800;
return $random_str;
}
// 调用函数生成token
$csrf_token = generate_csrf_token();
echo "生成的csrf token为:" . $csrf_token;
?>
在表单中嵌入csrf token
生成token后,需要在所有涉及数据修改的表单中嵌入token,通常放在隐藏字段中,示例代码如下:
<form method="post" action="update_info.php">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<label>用户名:<input type="text" name="username"></label><br>
<label>邮箱:<input type="email" name="email"></label><br>
<input type="submit" value="提交修改">
</form>
服务端校验csrf token
表单提交后,服务端需要校验请求中携带的token是否合法,校验逻辑包括token是否存在、是否和session中存储的一致、是否过期,示例代码如下:
<?php
session_start();
/**
* 校验csrf token
* @param string $input_token 请求中携带的token
* @return bool 校验通过返回true,否则返回false
*/
function verify_csrf_token($input_token) {
// 检查session中是否存在token和过期时间
if (!isset($_SESSION['csrf_token']) || !isset($_SESSION['csrf_token_expire'])) {
return false;
}
// 检查token是否过期
if (time() > $_SESSION['csrf_token_expire']) {
// 过期后清除session中的token信息
unset($_SESSION['csrf_token']);
unset($_SESSION['csrf_token_expire']);
return false;
}
// 校验token是否一致
if (hash_equals($_SESSION['csrf_token'], $input_token)) {
// 校验通过后可以清除当前token,避免重复使用
unset($_SESSION['csrf_token']);
unset($_SESSION['csrf_token_expire']);
return true;
}
return false;
}
// 处理表单提交请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input_token = $_POST['csrf_token'] ?? '';
if (verify_csrf_token($input_token)) {
// token校验通过,处理正常的业务逻辑
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
echo "用户信息修改成功,用户名为:" . htmlspecialchars($username) . ",邮箱为:" . htmlspecialchars($email);
} else {
// token校验失败,返回错误提示
http_response_code(403);
echo "请求校验失败,可能存在安全风险,请刷新页面后重试";
}
}
?>
ajax请求中的token处理
如果是ajax发起的post请求,需要在请求头或者请求参数中携带token,以下是使用原生js发送ajax请求的示例:
// 从页面的meta标签或者隐藏字段中获取token
var csrf_token = document.querySelector('input[name="csrf_token"]').value;
var xhr = new XMLHttpRequest();
xhr.open('POST', 'update_info.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
// 将token作为参数发送
xhr.send('csrf_token=' + encodeURIComponent(csrf_token) + '&username=test&email=test@ipipp.com');
注意事项
- token要存储在服务端的session中,不要直接存在cookie里,避免被攻击者获取。
- token要设置合理的有效期,一般建议15到30分钟,过期后需要重新生成。
- 校验token时要使用
hash_equals函数,避免时序攻击导致token被破解。 - 除了表单请求,所有涉及数据修改的接口比如删除、修改操作都需要添加csrf token校验。
- 如果是前后端分离的项目,可以将token放在响应头中返回给前端,前端存储后每次请求携带即可。