本文目录导读:

在 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、多步表单),可以告诉我,我可以给你定制代码。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播