在 PHP 中实现基于 Token 的认证机制,并在用户关闭页面后使 Token 失效,可以通过以下步骤实现:
在登录时生成 Token 并存储到数据库:
// 登录成功后的处理
function generateAndStoreToken($userId) {
// 生成唯一Token
$token = bin2hex(random_bytes(32)); // 生成64字符的随机字符串
// 计算过期时间(例如1小时后过期)
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
// 存储到数据库
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
$stmt = $db->prepare("INSERT INTO user_tokens (user_id, token, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$userId, $token, $expires]);
// 返回给客户端
return $token;
}
将 Token 返回给客户端,通常存储在:
localStorage (持久化存储,直到明确删除)
sessionStorage (标签页关闭后自动删除)
Cookie (可设置过期时间)
// 返回Token给客户端(JSON响应示例)
header('Content-Type: application/json');
echo json_encode([
'token' => $token,
'user_id' => $userId
]);
创建用于校验 Token 的中间件:
function validateToken($token) {
if (empty($token)) {
return false;
}
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
// 查询有效且未过期的Token
$stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()");
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
return $result['user_id']; // 返回用户ID表示验证通过
}
return false;
}
// 客户端存储Token到sessionStorage
sessionStorage.setItem('auth_token', token);
// 这样在关闭页面/标签后会自动清除
// 客户端定期发送心跳请求
setInterval(function() {
fetch('/api/heartbeat', {
headers: {
'Authorization': 'Bearer ' + sessionStorage.getItem('auth_token')
}
}).then(response => {
if (!response.ok) {
// Token可能已失效,跳转到登录页
window.location.href = '/login';
}
});
}, 300000); // 每5分钟一次
在数据库层面定期清理过期Token:
// 定时任务或每次Token验证时清理过期Token
function cleanupExpiredTokens() {
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
$db->exec("DELETE FROM user_tokens WHERE expires_at <= NOW()");
}
在需要验证的页面
// 获取Token(假设通过Authorization头传递)
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $token);
// 验证Token
$userId = validateToken($token);
if (!$userId) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// Token有效,继续处理请求
在 Token 认证系统中,当 Token 超过有效期后,可以通过以下几种方式实现 Token 更新:
这是最常用的 Token 更新方案,采用双 Token 机制:
// 登录时生成两种Token
function generateTokens($userId) {
// 访问Token (短期有效)
$accessToken = bin2hex(random_bytes(32));
$accessExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
// 刷新Token (长期有效)
$refreshToken = bin2hex(random_bytes(32));
$refreshExpires = date('Y-m-d H:i:s', strtotime('+30 days'));
// 存储到数据库
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
$stmt = $db->prepare("INSERT INTO user_tokens (user_id, access_token, refresh_token, access_expires, refresh_expires) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$userId, $accessToken, $refreshToken, $accessExpires, $refreshExpires]);
return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => 3600 // 1小时
];
}
// Token刷新接口
function refreshToken($refreshToken) {
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
// 验证refresh token是否有效
$stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE refresh_token = ? AND refresh_expires > NOW()");
$stmt->execute([$refreshToken]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
return false;
}
$userId = $result['user_id'];
// 生成新的access token
$newAccessToken = bin2hex(random_bytes(32));
$newAccessExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
// 更新数据库
$stmt = $db->prepare("UPDATE user_tokens SET access_token = ?, access_expires = ? WHERE refresh_token = ?");
$stmt->execute([$newAccessToken, $newAccessExpires, $refreshToken]);
return [
'access_token' => $newAccessToken,
'expires_in' => 3600
];
}
前端可以在检测到 Token 即将过期时自动刷新:
let authToken = localStorage.getItem('auth_token');
let refreshToken = localStorage.getItem('refresh_token');
function checkTokenExpiry() {
// 在实际应用中,你可能需要解码JWT或从API获取过期时间
// 这里假设我们有一个expiry时间存储在本地
const expiryTime = localStorage.getItem('token_expiry');
if (expiryTime && new Date(expiryTime) < new Date()) {
// Token已过期,使用refresh token获取新token
fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: refreshToken })
})
.then(response => response.json())
.then(data => {
localStorage.setItem('auth_token', data.access_token);
localStorage.setItem('token_expiry', new Date(Date.now() + data.expires_in * 1000));
})
.catch(error => {
// 刷新失败,跳转到登录页
window.location.href = '/login';
});
}
}
// 定期检查Token状态
setInterval(checkTokenExpiry, 60000); // 每分钟检查一次
在每次API请求时,后端可以检测Token状态并返回新Token:
function validateTokenWithRefresh($token) {
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
// 检查Token是否有效
$stmt = $db->prepare("SELECT user_id, refresh_token, expires_at FROM user_tokens WHERE access_token = ?");
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
return ['valid' => false];
}
$expiresAt = new DateTime($result['expires_at']);
$now = new DateTime();
if ($expiresAt > $now) {
// Token仍然有效
return ['valid' => true, 'user_id' => $result['user_id']];
} else {
// Token已过期,但可以刷新
$refreshResult = refreshToken($result['refresh_token']);
if ($refreshResult) {
return [
'valid' => false,
'should_refresh' => true,
'new_token' => $refreshResult['access_token']
];
} else {
return ['valid' => false];
}
}
}
// 在API中使用
$validation = validateTokenWithRefresh($token);
if (!$validation['valid']) {
if (isset($validation['should_refresh'])) {
// 返回新Token给客户端
header('X-New-Access-Token: ' . $validation['new_token']);
http_response_code(401); // 或考虑使用特殊状态码如 428 Precondition Required
echo json_encode(['error' => 'Token expired', 'new_token' => $validation['new_token']]);
} else {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
}
exit;
}
每次使用Token时延长其有效期:
function validateAndExtendToken($token) {
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
// 检查Token是否有效
$stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()");
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
// 延长Token有效期
$newExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt = $db->prepare("UPDATE user_tokens SET expires_at = ? WHERE token = ?");
$stmt->execute([$newExpires, $token]);
return $result['user_id'];
}
return false;
}
合理设置Token有效期:
Access Token: 短(15分钟-2小时)
Refresh Token: 长(7-30天)
安全存储Refresh Token:
使用HttpOnly、Secure Cookie存储
或在移动端使用安全存储机制
Refresh Token轮换:
每次使用Refresh Token获取新Access Token时,也生成新的Refresh Token
使旧的Refresh Token失效,增强安全性
实现Token撤销机制:
提供接口让用户可以主动撤销所有Token
在密码更改等敏感操作后自动撤销所有Token
限制Refresh Token使用频率:
防止暴力攻击
记录Refresh Token使用日志
通过以上机制,可以确保用户在保持活动状态时无需重新登录,同时在适当的时候更新Token保证安全性。