PHP 怎么PHP PKCE

wen PHP项目 2

本文目录导读:

PHP 怎么PHP PKCE

  1. 什么是PKCE?
  2. PHP实现PKCE的核心步骤
  3. 最佳实践

我将为您详细讲解如何在PHP中实现PKCE(Proof Key for Code Exchange)授权流程。

什么是PKCE?

PKCE是OAuth 2.0的扩展,主要用于公共客户端(如移动应用、SPA)防止授权码被拦截和滥用。

PHP实现PKCE的核心步骤

生成PKCE参数

<?php
class PKCEGenerator {
    /**
     * 生成PKCE代码验证器
     * 格式:43-128位随机字符串,仅包含[A-Z][a-z][0-9]及部分特殊字符
     */
    public static function generateCodeVerifier(): string {
        // 使用加密安全的随机数生成器
        $random = random_bytes(64); // 64字节=128字符
        return rtrim(strtr(base64_encode($random), '+/', '-_'), '=');
        // 限制在43-128字符之间
        // return substr($verifier, 0, 128);
    }
    /**
     * 生成代码挑战(基于SHA-256)
     */
    public static function generateCodeChallenge(string $codeVerifier): string {
        $hash = hash('sha256', $codeVerifier, true);
        return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
    }
    /**
     * 生成代码挑战(基于S256)
     */
    public static function generateS256Challenge(string $codeVerifier): string {
        return self::generateCodeChallenge($codeVerifier);
    }
    /**
     * 生成代码挑战(基于Plain,不推荐)
     */
    public static function generatePlainChallenge(string $codeVerifier): string {
        return $codeVerifier;
    }
}
?>

完整的PKCE授权流程实现

<?php
class PKCEExample {
    private $clientId;
    private $clientSecret;
    private $redirectUri;
    private $authorizationEndpoint;
    private $tokenEndpoint;
    private $scope;
    public function __construct(array $config) {
        $this->clientId = $config['client_id'];
        $this->clientSecret = $config['client_secret'];
        $this->redirectUri = $config['redirect_uri'];
        $this->authorizationEndpoint = $config['authorization_endpoint'];
        $this->tokenEndpoint = $config['token_endpoint'];
        $this->scope = $config['scope'] ?? '';
    }
    /**
     * 步骤1:生成Authorization请求
     */
    public function getAuthorizationUrl(): array {
        // 生成PKCE参数
        $codeVerifier = PKCEGenerator::generateCodeVerifier();
        $codeChallenge = PKCEGenerator::generateS256Challenge($codeVerifier);
        $state = bin2hex(random_bytes(16)); // CSRF防护
        // 保存到Session(或数据库)
        $_SESSION['pkce_verifier'] = $codeVerifier;
        $_SESSION['oauth_state'] = $state;
        // 构建Authorization URL
        $params = [
            'response_type' => 'code',
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUri,
            'scope' => $this->scope,
            'state' => $state,
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256'
        ];
        $url = $this->authorizationEndpoint . '?' . http_build_query($params);
        return [
            'url' => $url,
            'state' => $state
        ];
    }
    /**
     * 步骤2:处理回调并交换Token
     */
    public function exchangeCodeForToken(string $authorizationCode, string $state): array {
        // 验证State
        if ($state !== ($_SESSION['oauth_state'] ?? '')) {
            throw new Exception('State验证失败,可能存在CSRF攻击');
        }
        // 获取之前保存的code_verifier
        $codeVerifier = $_SESSION['pkce_verifier'] ?? '';
        if (empty($codeVerifier)) {
            throw new Exception('找不到code_verifier');
        }
        // 构建Token请求
        $tokenData = [
            'grant_type' => 'authorization_code',
            'code' => $authorizationCode,
            'redirect_uri' => $this->redirectUri,
            'client_id' => $this->clientId,
            'code_verifier' => $codeVerifier,
        ];
        // 如果有client_secret则添加
        if (!empty($this->clientSecret)) {
            $tokenData['client_secret'] = $this->clientSecret;
        }
        // 发送POST请求获取Token
        $response = $this->httpPost($this->tokenEndpoint, $tokenData);
        // 清理Session
        unset($_SESSION['pkce_verifier'], $_SESSION['oauth_state']);
        return $response;
    }
    /**
     * 简化版HTTP POST请求(实际生产中建议使用cURL或Guzzle)
     */
    private function httpPost(string $url, array $data): array {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/x-www-form-urlencoded',
            'Accept: application/json'
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        $decoded = json_decode($response, true);
        if ($httpCode >= 400 || (isset($decoded['error']) && $decoded['error'])) {
            throw new Exception('Token请求失败: ' . ($decoded['error_description'] ?? $response));
        }
        return $decoded;
    }
}
?>

完整的使用示例

<?php
// 启动Session
session_start();
// 配置
$config = [
    'client_id' => 'your_client_id',
    'client_secret' => 'your_client_secret', // 如果需要
    'redirect_uri' => 'https://your-app.com/callback',
    'authorization_endpoint' => 'https://auth.provider.com/authorize',
    'token_endpoint' => 'https://auth.provider.com/token',
    'scope' => 'read write'
];
$pkce = new PKCEExample($config);
// 处理授权请求
if (isset($_GET['login'])) {
    $result = $pkce->getAuthorizationUrl();
    header('Location: ' . $result['url']);
    exit;
}
// 处理回调
if (isset($_GET['code'])) {
    try {
        $token = $pkce->exchangeCodeForToken($_GET['code'], $_GET['state']);
        echo 'Access Token: ' . $token['access_token'];
        // 保存Token到安全存储
    } catch (Exception $e) {
        echo '授权失败: ' . $e->getMessage();
    }
}
?>

完整封装类(包含cURL实现)

<?php
class OAuth2Client {
    private $settings;
    public function __construct(array $settings) {
        $this->settings = $settings;
    }
    /**
     * 生成PKCE对
     */
    public function generatePKCE(): array {
        $verifier = $this->generateCodeVerifier();
        return [
            'verifier' => $verifier,
            'challenge' => $this->generateCodeChallenge($verifier)
        ];
    }
    /**
     * 获取授权URL
     */
    public function getAuthorizationUrl(): string {
        $pkce = $this->generatePKCE();
        $state = bin2hex(random_bytes(16));
        // 存储
        $_SESSION['oauth_state'] = $state;
        $_SESSION['oauth_verifier'] = $pkce['verifier'];
        $params = [
            'response_type' => 'code',
            'client_id' => $this->settings['client_id'],
            'redirect_uri' => $this->settings['redirect_uri'],
            'state' => $state,
            'code_challenge' => $pkce['challenge'],
            'code_challenge_method' => 'S256'
        ];
        if (!empty($this->settings['scope'])) {
            $params['scope'] = $this->settings['scope'];
        }
        return $this->settings['authorization_endpoint'] . '?' . http_build_query($params);
    }
    /**
     * 交换令牌
     */
    public function getAccessToken(string $code, string $state): array {
        // 验证状态
        if (!hash_equals($_SESSION['oauth_state'] ?? '', $state)) {
            throw new Exception('Invalid state');
        }
        $data = [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $this->settings['redirect_uri'],
            'client_id' => $this->settings['client_id'],
            'code_verifier' => $_SESSION['oauth_verifier']
        ];
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->settings['token_endpoint'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($data),
            CURLOPT_HTTPHEADER => [
                'Accept: application/json',
                'Content-Type: application/x-www-form-urlencoded'
            ]
        ]);
        $response = curl_exec($ch);
        $error = curl_error($ch);
        curl_close($ch);
        if ($error) {
            throw new Exception('cURL Error: ' . $error);
        }
        $tokenData = json_decode($response, true);
        if (isset($tokenData['error'])) {
            throw new Exception($tokenData['error_description'] ?? $tokenData['error']);
        }
        // 清理
        unset($_SESSION['oauth_state'], $_SESSION['oauth_verifier']);
        return $tokenData;
    }
    // 辅助方法
    private function generateCodeVerifier(): string {
        return rtrim(strtr(base64_encode(random_bytes(64)), '+/', '-_'), '=');
    }
    private function generateCodeChallenge(string $verifier): string {
        return rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
    }
}
?>

最佳实践

  1. 安全存储:使用Session或安全数据库存储code_verifier和state
  2. HTTPS:确保所有请求都通过HTTPS
  3. 校验State:始终验证state参数防止CSRF
  4. 清理Session:交换Token后清理敏感数据
  5. 错误处理:合理处理各种异常情况

这套实现符合OAuth 2.0 PKCE规范,适用于各种第三方认证服务。

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