PHP 怎么PHP 令牌刷新

wen PHP项目 2

本文目录导读:

PHP 怎么PHP 令牌刷新

  1. OAuth2 刷新令牌(Access Token 刷新)
  2. CSRF 令牌刷新
  3. JWT 令牌刷新
  4. 最佳实践
  5. 完整示例(API 端点)

在 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 连接。

抱歉,评论功能暂时关闭!