本文目录导读:

我来为你详细讲解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接口示例
- 安全优化建议
你可以根据实际需求选择适合的方案,主要考虑二维码的使用场景、并发量、是否需要长期跟踪等因素。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播