PHP 二维码过期处理

wen PHP项目 5

本文目录导读:

PHP 二维码过期处理

  1. 二维码过期处理的核心思路
  2. 完整实现方案
  3. 前端集成示例
  4. API接口示例
  5. 优化建议
  6. 前端展示效果

我来为你详细讲解PHP二维码过期的处理方法,包括完整的实现方案和代码示例。

二维码过期处理的核心思路

二维码过期的本质是存储一个带有效期的标识,然后在扫描时校验这个有效期,常见方案有:

  1. 数据库存储(最常用)
  2. Token签名+时间戳
  3. Redis缓存

完整实现方案

方案1:数据库存储方案(推荐)

数据库表结构

CREATE TABLE `qr_codes` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `qr_token` VARCHAR(64) NOT NULL UNIQUE COMMENT '二维码唯一标识',
  `payload_data` TEXT COMMENT '二维码携带的业务数据',
  `expire_time` DATETIME NOT NULL COMMENT '过期时间',
  `status` TINYINT DEFAULT 1 COMMENT '1-有效 0-已使用 -1-已过期',
  `qr_type` VARCHAR(32) COMMENT '二维码类型',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `use_time` DATETIME NULL COMMENT '使用时间',
  INDEX `idx_expire` (`expire_time`),
  INDEX `idx_type_status` (`qr_type`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

生成二维码并存储

<?php
class QrCodeService {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    /**
     * 生成带有效期的二维码
     * @param array $data 业务数据
     * @param int $expireSeconds 有效期(秒)
     * @return array 返回token和二维码内容
     */
    public function generateQrCode($data, $expireSeconds = 300) {
        // 生成唯一token
        $token = bin2hex(random_bytes(16));
        $expireTime = date('Y-m-d H:i:s', time() + $expireSeconds);
        // 存储到数据库
        $sql = "INSERT INTO qr_codes (qr_token, payload_data, expire_time, qr_type) 
                VALUES (:token, :data, :expire, :type)";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':token' => $token,
            ':data' => json_encode($data),
            ':expire' => $expireTime,
            ':type' => $data['type'] ?? 'default'
        ]);
        // 返回二维码内容(前端通过这个内容生成二维码图片)
        return [
            'token' => $token,
            'qr_content' => json_encode([
                'token' => $token,
                'timestamp' => time(),
                'expire' => $expireTime
            ]),
            'expire_time' => $expireTime
        ];
    }
    /**
     * 验证并处理二维码
     * @param string $token 二维码中的token
     * @return array 处理结果
     */
    public function validateAndUse($token) {
        try {
            // 查询二维码
            $sql = "SELECT * FROM qr_codes WHERE qr_token = :token";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([':token' => $token]);
            $qrCode = $stmt->fetch(PDO::FETCH_ASSOC);
            // 1. 检查是否存在
            if (!$qrCode) {
                return ['success' => false, 'message' => '二维码不存在'];
            }
            // 2. 检查是否已使用
            if ($qrCode['status'] == 0) {
                return ['success' => false, 'message' => '二维码已被使用'];
            }
            // 3. 检查是否过期
            if (strtotime($qrCode['expire_time']) < time()) {
                // 更新状态为已过期
                $this->updateStatus($token, -1);
                return ['success' => false, 'message' => '二维码已过期'];
            }
            // 4. 标记为已使用
            $this->updateStatus($token, 0);
            // 5. 返回业务数据
            return [
                'success' => true,
                'data' => json_decode($qrCode['payload_data'], true),
                'message' => '二维码验证成功'
            ];
        } catch (Exception $e) {
            return ['success' => false, 'message' => '系统错误:' . $e->getMessage()];
        }
    }
    private function updateStatus($token, $status) {
        $sql = "UPDATE qr_codes SET status = :status, use_time = NOW() 
                WHERE qr_token = :token";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':status' => $status,
            ':token' => $token
        ]);
    }
    /**
     * 清理过期二维码(定时任务调用)
     */
    public function cleanupExpired() {
        $sql = "UPDATE qr_codes SET status = -1 
                WHERE expire_time < NOW() AND status = 1";
        return $this->pdo->exec($sql);
    }
}

方案2:Token签名方案(无状态)

<?php
class TokenQrCode {
    private $secretKey;
    public function __construct($secretKey) {
        $this->secretKey = $secretKey;
    }
    /**
     * 生成带签名的二维码Token
     */
    public function generateToken($data, $expireSeconds = 300) {
        $timestamp = time();
        $expireTime = $timestamp + $expireSeconds;
        // 构建payload
        $payload = [
            'data' => $data,
            'exp' => $expireTime,
            'iat' => $timestamp,
            'nonce' => bin2hex(random_bytes(8))
        ];
        // 编码payload
        $payloadBase64 = $this->base64UrlEncode(json_encode($payload));
        // 生成签名
        $signature = hash_hmac('sha256', $payloadBase64, $this->secretKey);
        $signatureBase64 = $this->base64UrlEncode($signature);
        // 组合token
        $token = $payloadBase64 . '.' . $signatureBase64;
        return [
            'token' => $token,
            'expire_time' => date('Y-m-d H:i:s', $expireTime),
            'qr_content' => $token
        ];
    }
    /**
     * 验证Token并获取数据
     */
    public function verifyToken($token) {
        try {
            // 分割token
            $parts = explode('.', $token);
            if (count($parts) != 2) {
                return ['success' => false, 'message' => '无效的二维码'];
            }
            list($payloadBase64, $signatureBase64) = $parts;
            // 验证签名
            $expectedSignature = $this->base64UrlEncode(
                hash_hmac('sha256', $payloadBase64, $this->secretKey)
            );
            if (!hash_equals($expectedSignature, $signatureBase64)) {
                return ['success' => false, 'message' => '二维码被篡改'];
            }
            // 解码payload
            $payload = json_decode($this->base64UrlDecode($payloadBase64), true);
            // 检查过期时间
            if ($payload['exp'] < time()) {
                return ['success' => false, 'message' => '二维码已过期'];
            }
            // 检查是否已使用(通过nonce缓存防止重复使用)
            if ($this->isUsed($payload['nonce'])) {
                return ['success' => false, 'message' => '二维码已被使用'];
            }
            // 标记为已使用
            $this->markAsUsed($payload['nonce'], $payload['exp']);
            return [
                'success' => true,
                'data' => $payload['data'],
                'message' => '验证成功'
            ];
        } catch (Exception $e) {
            return ['success' => false, 'message' => '二维码无效'];
        }
    }
    private function isUsed($nonce) {
        // 存入Redis或数据库,设置为过期时间为payload中的exp
        // 这里以Redis为例
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        return $redis->exists('qr_nonce:' . $nonce);
    }
    private function markAsUsed($nonce, $expireTime) {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        $ttl = $expireTime - time();
        $redis->setex('qr_nonce:' . $nonce, $ttl, 'used');
    }
    private function base64UrlEncode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
    private function base64UrlDecode($data) {
        return base64_decode(strtr($data, '-_', '+/'));
    }
}

方案3:Redis缓存方案

<?php
class RedisQrCode {
    private $redis;
    public function __construct(Redis $redis) {
        $this->redis = $redis;
    }
    /**
     * 生成带有效期的二维码
     */
    public function generateQrCode($data, $expireSeconds = 300) {
        $token = md5(uniqid(mt_rand(), true) . microtime());
        // 存储到Redis,自动过期
        $qrKey = 'qr_code:' . $token;
        $qrData = [
            'data' => $data,
            'used' => false,
            'create_time' => time()
        ];
        $this->redis->setex($qrKey, $expireSeconds, json_encode($qrData));
        return [
            'token' => $token,
            'qr_content' => $token,
            'expire_seconds' => $expireSeconds
        ];
    }
    /**
     * 验证二维码
     */
    public function validateQrCode($token) {
        $qrKey = 'qr_code:' . $token;
        // 检查是否存在(Redis key过期即自动删除)
        if (!$this->redis->exists($qrKey)) {
            return ['success' => false, 'message' => '二维码已过期或不存在'];
        }
        // 获取数据
        $qrData = json_decode($this->redis->get($qrKey), true);
        // 检查是否已使用
        if ($qrData['used']) {
            return ['success' => false, 'message' => '二维码已被使用'];
        }
        // 标记为已使用
        $qrData['used'] = true;
        $this->redis->set($qrKey, json_encode($qrData));
        return [
            'success' => true,
            'data' => $qrData['data']
        ];
    }
}

前端集成示例

<!-- 前端生成二维码并处理 -->
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
</head>
<body>
    <div id="qrcode"></div>
    <div id="status"></div>
    <script>
    class QrCodeManager {
        constructor() {
            this.qrCode = document.getElementById('qrcode');
            this.status = document.getElementById('status');
            this.checkInterval = null;
        }
        // 生成二维码
        async generateQrCode() {
            const response = await fetch('/api/generate-qr', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({type: 'scan_login'})
            });
            const data = await response.json();
            // 生成二维码图片
            QRCode.toCanvas(this.qrCode, data.token, {
                width: 200,
                margin: 2
            });
            // 显示过期时间
            this.showCountdown(data.expire_time);
            // 启动轮询检查状态
            this.startPolling(data.token);
        }
        // 显示倒计时
        showCountdown(expireTime) {
            const expireTimestamp = new Date(expireTime).getTime();
            this.countdown = setInterval(() => {
                const remaining = expireTimestamp - Date.now();
                if (remaining <= 0) {
                    this.status.textContent = '二维码已过期';
                    this.stopPolling();
                    clearInterval(this.countdown);
                } else {
                    const seconds = Math.floor(remaining / 1000);
                    this.status.textContent = `剩余 ${seconds} 秒`;
                }
            }, 1000);
        }
        // 轮询检查二维码状态
        startPolling(token) {
            this.stopPolling();
            this.checkInterval = setInterval(async () => {
                const response = await fetch(`/api/check-qr-status?token=${token}`);
                const data = await response.json();
                if (data.success) {
                    this.status.textContent = '扫描成功!';
                    this.stopPolling();
                    clearInterval(this.countdown);
                    // 跳转到业务页面
                    window.location.href = data.redirect_url;
                }
            }, 3000); // 每3秒检查一次
        }
        stopPolling() {
            if (this.checkInterval) {
                clearInterval(this.checkInterval);
                this.checkInterval = null;
            }
        }
    }
    // 使用示例
    const manager = new QrCodeManager();
    manager.generateQrCode();
    </script>
</body>
</html>

API接口示例

<?php
// api.php
class ApiHandler {
    private $qrService;
    public function __construct($qrService) {
        $this->qrService = $qrService;
    }
    // 生成二维码接口
    public function generateQr() {
        header('Content-Type: application/json');
        $data = json_decode(file_get_contents('php://input'), true);
        // 设置有效期,比如登录二维码5分钟
        $result = $this->qrService->generateQrCode($data, 300);
        echo json_encode([
            'success' => true,
            'data' => $result
        ]);
    }
    // 验证二维码接口
    public function checkQrStatus() {
        header('Content-Type: application/json');
        $token = $_GET['token'] ?? '';
        // 检查二维码状态
        $result = $this->qrService->validateAndUse($token);
        if ($result['success']) {
            echo json_encode([
                'success' => true,
                'data' => $result['data']
            ]);
        } else {
            echo json_encode([
                'success' => false,
                'message' => $result['message']
            ]);
        }
    }
    // 处理扫描后的业务逻辑
    public function handleScan() {
        header('Content-Type: application/json');
        $token = $_POST['token'] ?? '';
        $result = $this->qrService->validateAndUse($token);
        if ($result['success']) {
            // 执行业务逻辑,比如登录、授权等
            $this->processBusinessLogic($result['data']);
        }
        echo json_encode($result);
    }
}

优化建议

定时清理过期数据

// cron.php - 每分钟执行一次
require 'config.php';
$pdo = new PDO($dsn, $user, $pass);
$qrService = new QrCodeService($pdo);
$qrService->cleanupExpired();

安全加固

// 限制单个IP生成频率
function checkRateLimit($ip) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $key = 'qr_rate:' . $ip;
    $count = $redis->incr($key);
    if ($count == 1) {
        $redis->expire($key, 3600); // 每小时重置
    }
    return $count <= 50; // 每小时最多50次
}

加密

function encryptQrData($data, $key) {
    $iv = openssl_random_pseudo_bytes(16);
    $encrypted = openssl_encrypt(
        json_encode($data),
        'AES-256-CBC',
        $key,
        0,
        $iv
    );
    return base64_encode($iv . $encrypted);
}

前端展示效果

/* 二维码样式 */
.qr-container {
    position: relative;
    width: 200px;
    height: 200px;
}
.qr-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 8px;
}
.qr-overlay.expired {
    color: red;
    font-size: 18px;
}
.countdown {
    margin-top: 10px;
    color: #666;
}

这个完整方案包含了:

  1. 多种过期处理实现方式
  2. 数据库和Redis两种存储方案
  3. 前端页面集成示例
  4. API接口示例
  5. 安全优化建议

你可以根据实际需求选择适合的方案,主要考虑二维码的使用场景、并发量、是否需要长期跟踪等因素。

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