PHP 怎么PHP 一次性令牌

wen PHP项目 3

本文目录导读:

PHP 怎么PHP 一次性令牌

  1. 基础 Session 一次性令牌(最常用)
  2. 令牌 + 超时机制
  3. 基于数据库的一次性令牌(适合 API / 多服务器)
  4. 高级:HMAC 签名令牌(无需存储)
  5. 常见问题与最佳实践
  6. 推荐选择

在 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、多步表单),可以告诉我,我可以给你定制代码。

抱歉,评论功能暂时关闭!