本文目录导读:

在 PHP 中实现令牌(Token)刷新通常指的是OAuth2 刷新令牌或CSRF 令牌刷新,以下是几种常见场景的完整解决方案:
OAuth2 刷新令牌(Access Token 刷新)
1 数据库表结构
CREATE TABLE oauth_tokens (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
access_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NOT NULL,
access_expires_at DATETIME NOT NULL,
refresh_expires_at DATETIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_access_token (access_token),
INDEX idx_refresh_token (refresh_token)
);
2 刷新令牌实现
<?php
class TokenManager {
private $db;
public function __construct($pdo) {
$this->db = $pdo;
}
// 生成新令牌
public function generateTokenPair($userId) {
$accessToken = bin2hex(random_bytes(32)); // 64字符
$refreshToken = bin2hex(random_bytes(32));
$accessExpires = date('Y-m-d H:i:s', time() + 3600); // 1小时有效期
$refreshExpires = date('Y-m-d H:i:s', time() + 2592000); // 30天有效期
$stmt = $this->db->prepare("
INSERT INTO oauth_tokens
(user_id, access_token, refresh_token, access_expires_at, refresh_expires_at)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$accessToken,
$refreshToken,
$accessExpires,
$refreshExpires
]);
return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'access_expires_in' => 3600,
'refresh_expires_in' => 2592000
];
}
// 刷新访问令牌
public function refreshAccessToken($refreshToken) {
// 查找有效的刷新令牌
$stmt = $this->db->prepare("
SELECT * FROM oauth_tokens
WHERE refresh_token = ?
AND refresh_expires_at > NOW()
AND revoked_at IS NULL
LIMIT 1
");
$stmt->execute([$refreshToken]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
throw new Exception('Refresh token无效或已过期');
}
// 撤销旧刷新令牌
$this->revokeToken($existing['id']);
// 生成新的令牌对
return $this->generateTokenPair($existing['user_id']);
}
// 撤销令牌
private function revokeToken($tokenId) {
$stmt = $this->db->prepare("
UPDATE oauth_tokens
SET revoked_at = NOW()
WHERE id = ?
");
$stmt->execute([$tokenId]);
}
}
3 使用示例
// 刷新端点到 REST API
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['action'] === 'refresh') {
header('Content-Type: application/json');
$json = json_decode(file_get_contents('php://input'), true);
$refreshToken = $json['refresh_token'] ?? null;
if (!$refreshToken) {
http_response_code(400);
echo json_encode(['error' => '缺少刷新令牌']);
exit;
}
try {
$tokenManager = new TokenManager($pdo);
$newTokens = $tokenManager->refreshAccessToken($refreshToken);
echo json_encode([
'success' => true,
'data' => $newTokens
]);
} catch (Exception $e) {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
CSRF 令牌刷新
1 Session 中的 CSRF 令牌
<?php
class CSRFProtection {
public static function generateToken() {
// 如果已有会话中的令牌,先销毁
if (isset($_SESSION['csrf_token'])) {
unset($_SESSION['csrf_token']);
}
// 生成新令牌
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
return $_SESSION['csrf_token'];
}
public static function validateToken($token) {
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
public static function refreshToken() {
return self::generateToken();
}
}
2 每次请求刷新 CSRF
// 页面加载时,生成或刷新令牌
session_start();
$csrfToken = CSRFProtection::generateToken();
?>
<!-- HTML 表单示例 -->
<form method="POST" action="/submit.php">
<input type="hidden" name="csrf_token" value="<?php echo $csrfToken; ?>">
<!-- 其他表单字段 -->
<button type="submit">提交</button>
</form>
<!-- AJAX 请求示例 -->
<button onclick="submitAjax()">AJAX提交</button>
<script>
async function submitAjax() {
const response = await fetch('/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': '<?php echo $csrfToken; ?>' // 前端需要保存这个值
},
body: JSON.stringify({ data: 'example' })
});
}
</script>
3 处理提交并刷新
// submit.php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrfToken = $_POST['csrf_token'] ?? null;
if (!CSRFProtection::validateToken($csrfToken)) {
die('CSRF验证失败');
}
// 处理表单数据...
// 刷新 CSRF 令牌(每次提交后自动刷新)
$newCsrfToken = CSRFProtection::refreshToken();
// 返回新令牌供前端使用
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'new_csrf_token' => $newCsrfToken
]);
}
JWT 令牌刷新
1 JWT 刷新实现
<?php
class JWTManager {
private $secretKey;
private $refreshtokenSecret;
public function __construct() {
$this->secretKey = 'your-access-token-secret';
$this->refreshtokenSecret = 'your-refresh-token-secret';
}
public function generateAccessToken($userId, $claims = []) {
$payload = array_merge([
'sub' => $userId,
'iat' => time(),
'exp' => time() + 3600, // 1小时过期
'type' => 'access'
], $claims);
return $this->createJWT($payload, $this->secretKey);
}
public function generateRefreshToken($userId) {
$payload = [
'sub' => $userId,
'iat' => time(),
'exp' => time() + 2592000, // 30天过期
'type' => 'refresh',
'jti' => bin2hex(random_bytes(16)) // 唯一ID,用于撤销
];
return $this->createJWT($payload, $this->refreshtokenSecret);
}
public function refreshAccessToken($refreshToken) {
// 验证刷新令牌
$decoded = $this->validateJWT($refreshToken, $this->refreshtokenSecret);
if (!$decoded) {
throw new Exception('刷新令牌无效');
}
if ($decoded->type !== 'refresh') {
throw new Exception('不是有效的刷新令牌');
}
// 检查是否在撤销列表中(实现时可存储到 Redis/数据库)
if ($this->isRevoked($decoded->jti)) {
throw new Exception('刷新令牌已被撤销');
}
// 生成新的访问令牌
return $this->generateAccessToken($decoded->sub);
}
private function createJWT($payload, $secret) {
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$base64Header = $this->base64UrlEncode($header);
$base64Payload = $this->base64UrlEncode(json_encode($payload));
$signature = hash_hmac('sha256',
"$base64Header.$base64Payload",
$secret,
true
);
$base64Signature = $this->base64UrlEncode($signature);
return "$base64Header.$base64Payload.$base64Signature";
}
private function validateJWT($jwt, $secret) {
$parts = explode('.', $jwt);
if (count($parts) !== 3) return false;
list($header, $payload, $signature) = $parts;
// 验证签名
$expectedSig = $this->base64UrlEncode(
hash_hmac('sha256', "$header.$payload", $secret, true)
);
if (!hash_equals($expectedSig, $signature)) {
return false;
}
// 验证过期时间
$decoded = json_decode($this->base64UrlDecode($payload));
if ($decoded->exp < time()) {
return false;
}
return $decoded;
}
private function base64UrlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private function base64UrlDecode($data) {
return base64_decode(strtr($data, '-_', '+/'));
}
private function isRevoked($jti) {
// 实现撤销逻辑:检查 Redis/数据库中的黑名单
return false; // 示例中默认不撤销
}
}
最佳实践
1 安全建议
- 令牌存储:使用
random_bytes()生成高强度随机令牌 - 令牌过期:访问令牌应设置较短有效期(15-60分钟),刷新令牌可长一些(7-30天)
- 令牌撤销:实现撤销机制,防止旧令牌被重用
- 使用 HTTPS:所有令牌传输必须加密
2 自动刷新逻辑(客户端)
// 前端自动刷新访问令牌
let isRefreshing = false;
async function makeAuthenticatedRequest(url, options = {}) {
let accessToken = localStorage.getItem('access_token');
options.headers = {
'Authorization': `Bearer ${accessToken}`,
...options.headers
};
let response = await fetch(url, options);
// 遇到 401 时尝试刷新
if (response.status === 401 && !isRefreshing) {
isRefreshing = true;
const refreshToken = localStorage.getItem('refresh_token');
const refreshResponse = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (refreshResponse.ok) {
const data = await refreshResponse.json();
localStorage.setItem('access_token', data.access_token);
// 重试原请求
options.headers['Authorization'] = `Bearer ${data.access_token}`;
response = await fetch(url, options);
} else {
// 退出登录
window.location.href = '/login';
}
isRefreshing = false;
}
return response;
}
完整示例(API 端点)
// api/refresh.php - 统一刷新端点
session_start();
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
// CSRF 验证(如适用)
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $input['csrf_token'])) {
http_response_code(403);
echo json_encode(['error' => 'CSRF验证失败']);
exit;
}
try {
$tokenManager = new TokenManager($pdo);
$refreshToken = $input['refresh_token'] ?? null;
if (!$refreshToken) {
throw new Exception('缺少刷新令牌');
}
$newTokens = $tokenManager->refreshAccessToken($refreshToken);
// 刷新 CSRF 令牌
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// 返回新令牌
echo json_encode([
'access_token' => $newTokens['access_token'],
'refresh_token' => $newTokens['refresh_token'],
'csrf_token' => $_SESSION['csrf_token'],
'expires_in' => 3600
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
这些实现覆盖了 PHP 中常见的令牌刷新场景,根据你的具体需求选择合适的方式,并确保始终使用安全的随机函数和 HTTPS 连接。