本文目录导读:

在 PHP 中生成一次性链接(一次性Token)是一种常见的做法,主要用于邮箱验证、密码重置、邀请链接等场景,以下是几种实现方式:
生成一次性Token的基本方法
<?php
// 方法1:使用 random_bytes(推荐)
$token = bin2hex(random_bytes(32));
// 方法2:使用 hash_hmac
$token = hash_hmac('sha256', uniqid(mt_rand(), true), 'your-secret-key');
// 方法3:使用 openssl(如果可用)
$token = bin2hex(openssl_random_pseudo_bytes(32));
?>
完整实现示例
数据库表设计
CREATE TABLE one_time_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(64) NOT NULL UNIQUE,
user_id INT NOT NULL,
type ENUM('email_verify', 'password_reset', 'invitation') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
used_at DATETIME DEFAULT NULL,
INDEX idx_token (token)
);
生成一次性链接的类
<?php
class OneTimeLink {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
/**
* 生成一次性链接
* @param int $userId 用户ID
* @param string $type 链接类型
* @param int $expireMinutes 过期时间(分钟)
* @return array 返回链接和Token
*/
public function generate($userId, $type, $expireMinutes = 60) {
// 生成Token
$token = bin2hex(random_bytes(32));
// 计算过期时间
$expiresAt = date('Y-m-d H:i:s', strtotime("+{$expireMinutes} minutes"));
// 保存到数据库
$stmt = $this->pdo->prepare(
"INSERT INTO one_time_tokens (token, user_id, type, expires_at)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([$token, $userId, $type, $expiresAt]);
// 构建链接
$link = "https://yourdomain.com/verify.php?token=$token";
return [
'token' => $token,
'link' => $link,
'expires_at' => $expiresAt
];
}
/**
* 验证并返回Token信息
* @param string $token 要验证的Token
* @return array|null 返回Token信息或null
*/
public function validate($token) {
// 查询Token
$stmt = $this->pdo->prepare(
"SELECT * FROM one_time_tokens
WHERE token = ? AND used_at IS NULL AND expires_at > NOW()"
);
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ?: null;
}
/**
* 使用Token(标记已使用)
* @param string $token Token值
* @return bool
*/
public function consume($token) {
$stmt = $this->pdo->prepare(
"UPDATE one_time_tokens
SET used_at = NOW()
WHERE token = ? AND used_at IS NULL"
);
return $stmt->execute([$token]);
}
}
?>
使用示例
<?php
// 1. 生成链接
$linkGenerator = new OneTimeLink($pdo);
$result = $linkGenerator->generate(
userId: 123,
type: 'email_verify',
expireMinutes: 30
);
// 发送邮件
$emailContent = "请点击以下链接完成邮箱验证(30分钟内有效):\n" . $result['link'];
sendEmail($userEmail, "邮箱验证", $emailContent);
// 2. 验证并处理请求
if (isset($_GET['token'])) {
$token = $_GET['token'];
// 验证Token
$tokenInfo = $linkGenerator->validate($token);
if ($tokenInfo) {
// Token有效,处理业务逻辑
$userId = $tokenInfo['user_id'];
// 更新用户状态
updateUserVerified($userId);
// 使用Token(一次性)
$linkGenerator->consume($token);
echo "验证成功!";
} else {
echo "链接无效或已过期";
}
}
?>
增强安全性
使用JWT(JSON Web Token)
<?php
require_once 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JWTOneTimeToken {
private $secretKey;
public function __construct($secretKey) {
$this->secretKey = $secretKey;
}
public function generate($userId, $expireMinutes = 60) {
$issuedAt = time();
$expiration = $issuedAt + $expireMinutes * 60;
$payload = [
'user_id' => $userId,
'iat' => $issuedAt,
'exp' => $expiration,
'jti' => bin2hex(random_bytes(16))
];
return JWT::encode($payload, $this->secretKey, 'HS256');
}
public function validate($token) {
try {
$decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
return (array)$decoded;
} catch (Exception $e) {
return null;
}
}
}
?>
带签名的一次性链接
<?php
class SignedOneTimeLink {
private $secretKey;
public function generate($userId, $action, $expireMinutes = 60) {
$timestamp = time();
$expiry = $timestamp + ($expireMinutes * 60);
// 构建数据
$data = "$userId|$action|$expiry";
// 生成签名
$signature = hash_hmac('sha256', $data, $this->secretKey);
// 构建链接
$url = "https://yourdomain.com/handle.php";
$params = http_build_query([
'user' => $userId,
'action' => $action,
'exp' => $expiry,
'sig' => $signature
]);
return "$url?$params";
}
public function validate($params) {
$signature = $params['sig'] ?? '';
unset($params['sig']);
// 重建数据
$data = implode('|', [
$params['user'],
$params['action'],
$params['exp']
]);
// 验证签名
$expected = hash_hmac('sha256', $data, $this->secretKey);
// 检查签名和过期时间
if (hash_equals($expected, $signature) && time() < $params['exp']) {
return true;
}
return false;
}
}
?>
数据库清理
定期清理过期的一次性链接:
<?php
// 删除过期的记录
function cleanupExpiredTokens($pdo) {
$stmt = $pdo->prepare(
"DELETE FROM one_time_tokens
WHERE expires_at < NOW() OR
(used_at IS NOT NULL AND used_at < DATE_SUB(NOW(), INTERVAL 24 HOUR))"
);
$stmt->execute();
}
// 定时任务:每天执行一次
cleanupExpiredTokens($pdo);
?>
注意事项
- Token长度:建议至少32字节(64个十六进制字符)
- 过期时间:根据业务需求设置合理的过期时间
- 单次使用:验证后立即标记为已使用
- 安全性:使用HTTPS传输链接
- 清理机制:定期清理过期和已使用的Token
这样实现的一次性链接既有安全性,又有灵活性,可以根据具体需求进行调整。