本文目录导读:

我来详细讲解PHP中JWT(JSON Web Token)的过期刷新机制实现。
JWT基础概念
1 JWT结构
// JWT由三部分组成 header.payload.signature // 示例 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
完整实现方案
1 基础JWT工具类
<?php
namespace App\Utils;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
class JwtHandler
{
private string $privateKey;
private string $publicKey;
private string $algorithm;
public function __construct()
{
$this->privateKey = file_get_contents('/path/to/private_key.pem');
$this->publicKey = file_get_contents('/path/to/public_key.pem');
$this->algorithm = 'RS256';
}
/**
* 生成访问令牌
*/
public function generateAccessToken(array $payload): string
{
$now = time();
$tokenData = array_merge($payload, [
'iat' => $now, // 签发时间
'nbf' => $now, // 生效时间
'exp' => $now + 3600, // 过期时间(1小时)
'jti' => bin2hex(random_bytes(16)), // 唯一标识
'type' => 'access', // 令牌类型
]);
return JWT::encode($tokenData, $this->privateKey, $this->algorithm);
}
/**
* 生成刷新令牌(长期有效)
*/
public function generateRefreshToken(array $payload): string
{
$now = time();
$tokenData = array_merge($payload, [
'iat' => $now,
'nbf' => $now,
'exp' => $now + (7 * 24 * 3600), // 7天过期
'jti' => bin2hex(random_bytes(16)),
'type' => 'refresh',
'scope' => 'refresh_token',
]);
return JWT::encode($tokenData, $this->privateKey, $this->algorithm);
}
/**
* 验证令牌
*/
public function validateToken(string $token, string $expectedType = 'access'): ?array
{
try {
$decoded = JWT::decode($token, new Key($this->publicKey, $this->algorithm));
// 验证令牌类型
if ($decoded->type !== $expectedType) {
return null;
}
// 检查是否被撤销(可选)
if ($this->isBlacklisted($decoded->jti)) {
return null;
}
return (array) $decoded;
} catch (ExpiredException $e) {
// 令牌过期
return ['error' => 'token_expired'];
} catch (\Exception $e) {
// 其他错误
return null;
}
}
/**
* 解析令牌(不验证过期时间)
*/
public function decodeToken(string $token): array
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return [];
}
$payload = base64_decode(strtr($parts[1], '-_', '+/'));
return json_decode($payload, true);
}
private function isBlacklisted(string $jti): bool
{
// 检查Redis或数据库中是否有撤销记录
return false;
}
}
2 令牌管理服务
<?php
namespace App\Services;
use App\Utils\JwtHandler;
use App\Models\RefreshToken;
use Predis\Client;
use Carbon\Carbon;
class TokenService
{
private JwtHandler $jwtHandler;
private Client $redis;
public function __construct(JwtHandler $jwtHandler, Client $redis)
{
$this->jwtHandler = $jwtHandler;
$this->redis = $redis;
}
/**
* 生成令牌对
*/
public function generateTokenPair(array $userData): array
{
// 基础用户信息
$payload = [
'user_id' => $userData['id'],
'username' => $userData['username'],
'role' => $userData['role'] ?? 'user'
];
$accessToken = $this->jwtHandler->generateAccessToken($payload);
$refreshToken = $this->jwtHandler->generateRefreshToken($payload);
// 存储刷新令牌
$this->storeRefreshToken([
'token' => $refreshToken,
'user_id' => $userData['id'],
'expires_at' => time() + (7 * 24 * 3600),
'device_info' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
]);
return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => 3600,
'token_type' => 'Bearer'
];
}
/**
* 刷新访问令牌
*/
public function refreshAccessToken(string $refreshToken): ?array
{
// 验证刷新令牌
$decoded = $this->jwtHandler->validateToken($refreshToken, 'refresh');
if (!$decoded || isset($decoded['error'])) {
return null;
}
// 检查是否已被使用(防止重放攻击)
if (!$this->isRefreshTokenValid($decoded['jti'])) {
return null;
}
// 获取用户数据
$userId = $decoded['user_id'];
// 生成新的访问令牌(保留刷新令牌)
$payload = [
'user_id' => $userId,
'username' => $decoded['username'],
'role' => $decoded['role']
];
$newAccessToken = $this->jwtHandler->generateAccessToken($payload);
// 可选的:轮换刷新令牌
$newRefreshToken = $this->rotateRefreshToken($refreshToken, $decoded['jti'], $userId);
return [
'access_token' => $newAccessToken,
'refresh_token' => $newRefreshToken ?? $refreshToken,
'expires_in' => 3600,
'token_type' => 'Bearer'
];
}
/**
* 轮换刷新令牌
*/
private function rotateRefreshToken(string $oldToken, string $oldJti, int $userId): ?string
{
// 撤销旧令牌
$this->revokeRefreshToken($oldJti);
// 生成新刷新令牌
$newRefreshToken = $this->jwtHandler->generateRefreshToken([
'user_id' => $userId
]);
// 存储新令牌
$this->storeRefreshToken([
'token' => $newRefreshToken,
'user_id' => $userId,
'expires_at' => time() + (7 * 24 * 3600)
]);
return $newRefreshToken;
}
/**
* 存储刷新令牌
*/
private function storeRefreshToken(array $data): void
{
$decoded = $this->jwtHandler->decodeToken($data['token']);
$tokenRecord = new RefreshToken([
'token_hash' => hash('sha256', $data['token']),
'user_id' => $data['user_id'],
'client_id' => $decoded['jti'],
'expires_at' => Carbon::createFromTimestamp($data['expires_at']),
'revoked' => false,
'device_info' => $data['device_info'] ?? null
]);
$tokenRecord->save();
// 同时存储到Redis用于快速访问
$this->redis->setex(
"refresh_token:{$decoded['jti']}",
7 * 24 * 3600,
json_encode([
'user_id' => $data['user_id'],
'revoked' => false
])
);
}
/**
* 检查刷新令牌是否有效
*/
private function isRefreshTokenValid(string $jti): bool
{
$tokenData = json_decode($this->redis->get("refresh_token:{$jti}"), true);
if (!$tokenData || $tokenData['revoked']) {
return false;
}
return true;
}
/**
* 撤销刷新令牌
*/
private function revokeRefreshToken(string $jti): void
{
$this->redis->setex(
"refresh_token:{$jti}",
7 * 24 * 3600,
json_encode(['revoked' => true])
);
// 同时更新数据库记录
RefreshToken::where('client_id', $jti)
->update(['revoked' => true, 'revoked_at' => Carbon::now()]);
}
/**
* 撤销所有用户令牌(强制退出)
*/
public function revokeUserTokens(int $userId): void
{
$tokens = RefreshToken::where('user_id', $userId)
->where('revoked', false)
->get();
foreach ($tokens as $token) {
$this->revokeRefreshToken($token->client_id);
}
// 撤销访问令牌(通过Redis黑名单)
$this->redis->sadd("user_blacklist:{$userId}", time());
$this->redis->expire("user_blacklist:{$userId}", 3600);
}
}
3 认证中间件
<?php
namespace App\Http\Middleware;
use App\Utils\JwtHandler;
use App\Services\TokenService;
use Firebase\JWT\ExpiredException;
use Exception;
class Authenticate
{
private JwtHandler $jwtHandler;
private TokenService $tokenService;
public function __construct(JwtHandler $jwtHandler, TokenService $tokenService)
{
$this->jwtHandler = $jwtHandler;
$this->tokenService = $tokenService;
}
/**
* 处理请求
*/
public function handle($request, $next)
{
$authHeader = $request->getHeader('Authorization');
if (!$authHeader || !preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
return $this->unauthorized('未提供有效的认证令牌');
}
$accessToken = $matches[1];
// 验证访问令牌
$decoded = $this->jwtHandler->validateToken($accessToken, 'access');
if (!$decoded) {
return $this->unauthorized('无效的访问令牌');
}
if (isset($decoded['error']) && $decoded['error'] === 'token_expired') {
// 检查是否有刷新令牌
$refreshToken = $request->getParam('refresh_token');
if (!$refreshToken) {
return $this->tokenExpired('访问令牌已过期');
}
// 尝试刷新令牌
$newTokens = $this->tokenService->refreshAccessToken($refreshToken);
if (!$newTokens) {
return $this->unauthorized('刷新令牌无效');
}
// 设置新的令牌
$request->setTokens($newTokens);
// 继续处理请求
$request->setUserData($decoded);
// 在响应中添加新的令牌
$response = $next($request);
$response->headers['X-New-Tokens'] = json_encode($newTokens);
return $response;
}
// 设置用户信息到请求
$request->setUserData($decoded);
return $next($request);
}
private function unauthorized(string $message)
{
return [
'status' => 401,
'error' => 'unauthorized',
'message' => $message
];
}
private function tokenExpired(string $message)
{
return [
'status' => 401,
'error' => 'token_expired',
'message' => $message,
'token_expired' => true
];
}
}
4 刷新端点控制器
<?php
namespace App\Controllers;
use App\Services\TokenService;
use App\Utils\JwtHandler;
class AuthController
{
private TokenService $tokenService;
private JwtHandler $jwtHandler;
public function __construct(TokenService $tokenService, JwtHandler $jwtHandler)
{
$this->tokenService = $tokenService;
$this->jwtHandler = $jwtHandler;
}
/**
* 刷新令牌端点
*/
public function refreshToken(array $request, array $server): array
{
// 从请求中获取刷新令牌
$refreshToken = $request['refresh_token'] ?? null;
if (!$refreshToken) {
return [
'status' => 400,
'body' => [
'error' => 'missing_refresh_token',
'message' => '缺少刷新令牌'
]
];
}
// 刷新访问令牌
$newTokens = $this->tokenService->refreshAccessToken($refreshToken);
if (!$newTokens) {
return [
'status' => 401,
'body' => [
'error' => 'invalid_refresh_token',
'message' => '刷新令牌无效或已过期'
]
];
}
return [
'status' => 200,
'body' => $newTokens
];
}
/**
* 登出(撤销令牌)
*/
public function logout(array $request, array $server): array
{
$authHeader = $server['HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
$accessToken = $matches[1];
$userData = $this->jwtHandler->decodeToken($accessToken);
// 撤销用户所有令牌
if (isset($userData['user_id'])) {
$this->tokenService->revokeUserTokens($userData['user_id']);
}
}
return [
'status' => 200,
'body' => ['message' => '成功登出']
];
}
}
5 客户端集成示例
<?php
// 创建API客户端封装
class ApiClient
{
private string $accessToken;
private string $refreshToken;
private string $baseUrl;
public function __construct(string $baseUrl)
{
$this->baseUrl = $baseUrl;
$this->loadTokens(); // 从存储中加载令牌
}
/**
* 发送带认证的请求
*/
public function request(string $method, string $endpoint, array $data = []): array
{
$attempts = 0;
while ($attempts < 2) {
$response = $this->sendRequest($method, $endpoint, $data);
// 检查是否过期
if ($response['status'] === 401 && isset($response['error']) && $response['error'] === 'token_expired') {
// 尝试刷新令牌
if ($this->refreshToken) {
$refreshed = $this->refreshAccessToken();
if ($refreshed) {
$attempts++;
continue;
}
}
}
return $response;
}
return ['error' => '认证失败'];
}
/**
* 刷新访问令牌
*/
private function refreshAccessToken(): bool
{
$ch = curl_init($this->baseUrl . '/auth/refresh');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'refresh_token' => $this->refreshToken
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 200) {
$data = json_decode($response, true);
$this->accessToken = $data['access_token'];
$this->refreshToken = $data['refresh_token'];
$this->saveTokens(); // 存储新令牌
return true;
}
return false;
}
private function sendRequest(string $method, string $endpoint, array $data): array
{
$ch = curl_init($this->baseUrl . $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->accessToken,
'Content-Type: application/json'
]);
if ($method === 'GET') {
curl_setopt($ch, CURLOPT_HTTPGET, true);
} else if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array_merge(
json_decode($response, true) ?? [],
['status' => $status]
);
}
}
数据库结构
-- 刷新令牌表
CREATE TABLE refresh_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
client_id VARCHAR(100) NOT NULL, -- JWT的jti
token_hash VARCHAR(255) NOT NULL, -- 令牌的SHA256哈希
expires_at DATETIME NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
revoked_at DATETIME NULL,
device_info VARCHAR(255) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_client_id (client_id),
INDEX idx_revoked (revoked),
UNIQUE KEY uniq_token_hash (token_hash)
);
-- 黑名单表(用于撤销访问令牌)
CREATE TABLE token_blacklist (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
jti VARCHAR(100) NOT NULL UNIQUE,
user_id BIGINT UNSIGNED NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_expires_at (expires_at)
);
安全建议
1 关键安全措施
// 1. 请求限制
class RateLimit
{
private $redis;
public function checkRefreshLimit(int $userId, int $max = 10)
{
$key = "refresh_limit:{$userId}:" . date('Y-m-d-H');
$count = $this->redis->incr($key);
$this->redis->expire($key, 3600);
if ($count > $max) {
throw new Exception('刷新次数过多');
}
}
}
// 2. 客户端检测
class DeviceFingerprint
{
public static function create(array $server): string
{
$data = [
$server['HTTP_USER_AGENT'] ?? '',
$server['REMOTE_ADDR'] ?? '',
$server['HTTP_ACCEPT_LANGUAGE'] ?? ''
];
return hash('sha256', implode('|', $data));
}
}
// 3. 安全配置
class SecurityConfig
{
public const CONFIG = [
// 访问令牌有效期
'access_token_ttl' => 900, // 15分钟
'refresh_token_ttl' => 604800, // 7天
'refresh_token_rotate' => true, // 每次刷新轮换
'max_concurrent_tokens' => 5, // 最大并发令牌
'blacklist_expired' => 60, // 黑名单过期时间(分钟)
'enable_device_fingerprint' => true, // 启用设备指纹
'min_refresh_interval' => 60, // 最小刷新间隔(秒)
];
}
2 实现最佳实践
class TokenBestPractices
{
/**
* 1. 使用短生命周期的访问令牌
*/
public function example1(): void
{
// 访问令牌:15分钟过期
$accessToken = $this->generateAccessToken($payload, 900);
// 刷新令牌:7天,可轮换
$refreshToken = $this->generateRefreshToken($payload, 604800);
}
/**
* 2. 实现令牌轮换
*/
public function example2(TokenStorage $storage): void
{
// 检测到生成过多新令牌时撤销用户所有令牌
$tokenCount = $storage->countActiveTokens($userId);
if ($tokenCount > 10) {
$storage->revokeAllUserTokens($userId);
// 要求用户重新登录
}
}
/**
* 3. 实现即时撤销
*/
public function example3(TokenStorage $storage): void
{
// 密码更改后
$storage->revokeAllUserTokens($userId);
// 添加到全局黑名单
$storage->blacklistUser($userId, 3600);
}
/**
* 4. 使用滑动过期时间
*/
public function example4(SessionManager $session): void
{
// 如果用户持续活跃,刷新过期时间
if ($session->isActive()) {
$session->refreshExpiration(30 * 60); // 续期30分钟
}
}
}
完整的中间件实现
<?php
namespace App\Http\Middleware;
use Closure;
use App\Services\TokenService;
use App\Validators\TokenValidator;
class TokenRefreshMiddleware
{
private TokenService $tokenService;
private TokenValidator $validator;
public function handle($request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
try {
// 验证访问令牌
$payload = $this->validator->validateAccessToken($token);
$request->attributes->set('user', $payload);
} catch (AccessTokenExpiredException $e) {
// 访问令牌过期,尝试刷新
$refreshToken = $request->cookie('refresh_token')
?? $request->header('X-Refresh-Token');
if (!$refreshToken) {
return response()->json([
'error' => 'token_expired',
'message' => '请重新登录'
], 401);
}
try {
$newTokens = $this->tokenService->refreshTokens($refreshToken);
// 设置新令牌到响应
$response = $next($request);
// 通过响应头或Cookie发送新令牌
return $this->attachNewTokens($response, $newTokens);
} catch (RefreshTokenInvalidException $e) {
return response()->json([
'error' => 'refresh_token_invalid',
'message' => '刷新令牌无效'
], 401);
}
} catch (InvalidTokenException $e) {
return response()->json(['error' => 'Invalid token'], 401);
}
return $next($request);
}
private function attachNewTokens($response, array $tokens)
{
$response->headers->set('X-Access-Token', $tokens['access_token']);
if (isset($tokens['refresh_token'])) {
$response->headers->setCookie(
cookie('refresh_token', $tokens['refresh_token'], 604800, '/', null, true, true, false, 'Strict')
);
}
return $response;
}
}
测试用例
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use App\Services\TokenService;
use App\Utils\JwtHandler;
class TokenRefreshTest extends TestCase
{
private TokenService $tokenService;
protected function setUp(): void
{
$this->tokenService = new TokenService(
new JwtHandler(),
$this->createMock(\Predis\Client::class)
);
}
public function testRefreshTokenRotation()
{
// 初始令牌
$userData = ['id' => 1, 'username' => 'test'];
$tokens = $this->tokenService->generateTokenPair($userData);
// 模拟请求刷新
$newTokens = $this->tokenService->refreshAccessToken($tokens['refresh_token']);
$this->assertNotEquals($tokens['refresh_token'], $newTokens['refresh_token']);
$this->assertNotEquals($tokens['access_token'], $newTokens['access_token']);
}
public function testExpiredAccessTokenRefresh()
{
// 创建过期访问令牌
$expiredToken = $this->createExpiredAccessToken();
// 验证令牌无效
$result = $this->tokenService->refreshAccessToken($expiredToken);
$this->assertNull($result);
}
public function testRevokedTokenRejection()
{
// 生成令牌
$tokens = $this->tokenService->generateTokenPair(['id' => 1, 'username' => 'test']);
// 模拟撤销
$this->tokenService->revokeUserTokens(1);
// 使用已撤销的刷新令牌
$result = $this->tokenService->refreshAccessToken($tokens['refresh_token']);
$this->assertNull($result);
}
private function createExpiredAccessToken(): string
{
$now = time();
$payload = [
'user_id' => 1,
'username' => 'test',
'iat' => $now - 7200,
'nbf' => $now - 7200,
'exp' => $now - 3600,
'jti' => bin2hex(random_bytes(16)),
'type' => 'access'
];
return JWT::encode($payload, $this->privateKey, 'RS256');
}
}
最佳实践总结
关键点:
- 访问令牌和刷新令牌分离
- 刷新令牌轮换机制
- 并发安全处理
- 设备指纹识别
- 速率限制
- 令牌撤销机制
安全事项:
- 使用HTTPS
- 访问令牌短期+刷新令牌长期
- 实现令牌轮换
- 检测异常行为
- 用户操作后立即撤销
性能优化:
- Redis缓存令牌状态
- 黑名单过期自动清理
- 批量处理过期令牌
这个完整的实现方案提供了生产级的JWT过期刷新机制,包含了安全处理、性能和可用性的平衡。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播