本文目录导读:

我来为你详细讲解PHP二维码过期的处理方法,包括完整的实现方案和代码示例。
二维码过期处理的核心思路
二维码过期的本质是存储一个带有效期的标识,然后在扫描时校验这个有效期,常见方案有:
- 数据库存储(最常用)
- Token签名+时间戳
- 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;
}
这个完整方案包含了:
- 多种过期处理实现方式
- 数据库和Redis两种存储方案
- 前端页面集成示例
- API接口示例
- 安全优化建议
你可以根据实际需求选择适合的方案,主要考虑二维码的使用场景、并发量、是否需要长期跟踪等因素。