本文目录导读:

在 PHP 中实现一次性令牌(CSRF Token / One-time Token)通常用于防止跨站请求伪造(CSRF)攻击或确保表单的唯一提交(防重复提交)。
下面我会给出 4 种最常用、由浅入深的实现方式,包括代码示例。
基础 Session 一次性令牌(最常用)
原理: 将令牌存入 $_SESSION,表单提交时比对。
<?php
session_start();
// 生成令牌
function generateToken() {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
// 验证并销毁令牌(一次性)
function verifyToken($userToken) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
$stored = $_SESSION['csrf_token'];
// 验证后立即删除(实现一次性)
unset($_SESSION['csrf_token']);
return hash_equals($stored, $userToken);
}
// 使用示例:显示表单
$token = generateToken();
?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
<button type="submit">提交</button>
</form>
<?php
// 处理提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userToken = $_POST['csrf_token'] ?? '';
if (verifyToken($userToken)) {
echo "✅ 验证通过,处理业务逻辑";
} else {
echo "❌ 无效或过期的令牌";
}
}
?>
优点: 简单、安全
缺点: 用户按“返回”后退重提交会失败(这正是目的)
令牌 + 超时机制
在 Session 中同时存储令牌的生成时间,过期后自动失效。
<?php
session_start();
function generateToken($expireSeconds = 1800) {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = [
'value' => $token,
'expire' => time() + $expireSeconds
];
return $token;
}
function verifyToken($userToken) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
$data = $_SESSION['csrf_token'];
// 检查是否过期
if (time() > $data['expire']) {
unset($_SESSION['csrf_token']);
return false;
}
$valid = hash_equals($data['value'], $userToken);
unset($_SESSION['csrf_token']); // 一次性使用
return $valid;
}
基于数据库的一次性令牌(适合 API / 多服务器)
场景: 跨服务器、无需 Session、或需要记录令牌用途。
// 假设已连接 $pdo (PDO)
function generateDbToken($pdo, $userId, $purpose = 'form_submit') {
$token = bin2hex(random_bytes(32));
$stmt = $pdo->prepare(
"INSERT INTO one_time_tokens (token, user_id, purpose, created_at)
VALUES (?, ?, ?, NOW())"
);
$stmt->execute([$token, $userId, $purpose]);
return $token;
}
function verifyDbToken($pdo, $token, $userId, $purpose = 'form_submit') {
$stmt = $pdo->prepare(
"SELECT id, created_at FROM one_time_tokens
WHERE token = ? AND user_id = ? AND purpose = ? AND used = 0
LIMIT 1"
);
$stmt->execute([$token, $userId, $purpose]);
$row = $stmt->fetch();
if (!$row) {
return false;
}
// 标记为已使用(一次性)
$update = $pdo->prepare("UPDATE one_time_tokens SET used = 1 WHERE id = ?");
$update->execute([$row['id']]);
return true;
}
数据库表结构:
CREATE TABLE one_time_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
purpose VARCHAR(50) DEFAULT 'form_submit',
used TINYINT DEFAULT 0,
created_at DATETIME
);
高级:HMAC 签名令牌(无需存储)
原理: 不存储令牌,通过密钥签名生成,然后验证签名。
define('TOKEN_SECRET', 'your-secret-key-here-change-it');
function generateSignedToken() {
$data = session_id() . '|' . time();
$signature = hash_hmac('sha256', $data, TOKEN_SECRET);
return base64_encode($data . '|' . $signature);
}
function verifySignedToken($tokenRaw) {
$decoded = base64_decode($tokenRaw, true);
if (!$decoded || substr_count($decoded, '|') !== 2) {
return false;
}
[$sid, $time, $signature] = explode('|', $decoded);
// 验证 session 一致
if ($sid !== session_id()) {
return false;
}
// 验证签名
$expected = hash_hmac('sha256', "$sid|$time", TOKEN_SECRET);
if (!hash_equals($expected, $signature)) {
return false;
}
// 可选:检查时间窗口(30 分钟有效)
return (time() - (int)$time) < 1800;
}
优点: 无需 Session 存储,无状态
缺点: 无法真正做到“一次性”,除非额外记录已使用列表
常见问题与最佳实践
| 问题 | 建议 |
|---|---|
| 用户打开多个标签页 | 在 Session 中保留最后生成的令牌即可,前一个会被覆盖失效 |
| AJAX 请求 | 把令牌放在 HTTP 响应头 X-CSRF-Token 中,前端获取后传回 |
| 令牌被盗? | 令牌绑定 Session ID 或 IP(IP 变动大,慎用) |
| 需要“多次有效” | 改为使用 Token 池,维护一个令牌列表而不是单个值 |
推荐选择
- 简单表单防重 / CSRF → 第 1 种(Session 一次性)
- 长时间操作(支付、注册)→ 第 2 种(加过期时间)
- 无状态 API → 第 4 种(HMAC 签名)
- 大型系统 / 微服务 → 第 3 种(数据库)
如果你有具体的使用场景(如购物车、API、多步表单),可以告诉我,我可以给你定制代码。