PHP 客户端凭证管理

wen PHP项目 7

本文目录导读:

PHP 客户端凭证管理

  1. 密码学安全存储(核心原则)
  2. OAuth 2.0 客户端凭证模式实现
  3. 实际使用示例
  4. 安全加固清单
  5. API 密钥简单场景
  6. 项目结构建议
  7. 高级:Token 持久化到 Redis
  8. 测试建议

在 PHP 中管理客户端凭证(Client Credentials)通常涉及OAuth 2.0 客户端凭证模式(用于服务间通信)或API 密钥管理,以下是系统化的最佳实践和代码示例:


密码学安全存储(核心原则)

绝不硬编码凭证在代码中,使用环境变量或专用的机密管理服务。

方法 A:环境变量(推荐用于大多数项目)

// config/credentials.php
<?php
return [
    'client_id' => getenv('API_CLIENT_ID') ?: null,
    'client_secret' => getenv('API_CLIENT_SECRET') ?: null,
    'token_endpoint' => getenv('OAUTH_TOKEN_URL') ?: 'https://api.example.com/oauth/token',
    'scope' => getenv('OAUTH_SCOPE') ?: 'read write',
];

方法 B:Vault 或云密钥管理(生产环境)

// 使用 AWS Secrets Manager 示例
use Aws\SecretsManager\SecretsManagerClient;
function getSecret(string $secretName): array {
    $client = new SecretsManagerClient([
        'version' => 'latest',
        'region' => 'us-east-1'
    ]);
    $result = $client->getSecretValue(['SecretId' => $secretName]);
    return json_decode($result['SecretString'], true);
}
// 使用: $creds = getSecret('my-app-credentials');

OAuth 2.0 客户端凭证模式实现

<?php
class OAuthClient {
    private string $clientId;
    private string $clientSecret;
    private string $tokenEndpoint;
    private ?array $cachedToken = null;
    private int $tokenExpiryTime = 0;
    public function __construct(array $config) {
        $this->clientId = $config['client_id'];
        $this->clientSecret = $config['client_secret'];
        $this->tokenEndpoint = $config['token_endpoint'];
    }
    /**
     * 获取访问令牌(带缓存)
     */
    public function getAccessToken(): string {
        // 如果令牌还有效(提前5分钟刷新),直接返回
        if ($this->cachedToken && (time() < $this->tokenExpiryTime - 300)) {
            return $this->cachedToken;
        }
        return $this->requestNewToken();
    }
    private function requestNewToken(): string {
        $ch = curl_init($this->tokenEndpoint);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_POSTFIELDS => http_build_query([
                'grant_type' => 'client_credentials',
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret,
                'scope' => 'read write'
            ]),
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 10
        ]);
        $response = curl_exec($ch);
        $error = curl_error($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($error) {
            throw new RuntimeException("cURL Error: {$error}");
        }
        $data = json_decode($response, true);
        if ($httpCode !== 200 || !isset($data['access_token'])) {
            throw new RuntimeException(
                "Token request failed (HTTP {$httpCode}): " . json_encode($data)
            );
        }
        $this->cachedToken = $data['access_token'];
        $this->tokenExpiryTime = time() + ($data['expires_in'] ?? 3600);
        return $this->cachedToken;
    }
    /**
     * 执行带认证的 API 请求
     */
    public function makeAuthenticatedRequest(string $url, string $method = 'GET', array $data = []): array {
        $token = $this->getAccessToken();
        $ch = curl_init($url);
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json'
            ],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_CUSTOMREQUEST => $method
        ];
        if (!empty($data) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            $options[CURLOPT_POSTFIELDS] = json_encode($data);
        }
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);
        $error = curl_error($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($error) {
            throw new RuntimeException("API Request Error: {$error}");
        }
        return [
            'http_code' => $httpCode,
            'data' => json_decode($response, true)
        ];
    }
}

实际使用示例

// 初始化客户端
$client = new OAuthClient(require 'config/credentials.php');
try {
    $result = $client->makeAuthenticatedRequest(
        'https://api.example.com/v1/data',
        'POST',
        ['key' => 'value']
    );
    // 处理 401 重试(令牌可能刚过期)
    if ($result['http_code'] === 401) {
        $this->refreshTokenForced(); // 强制刷新
        $result = $client->makeAuthenticatedRequest(...); // 重试
    }
    print_r($result);
} catch (Exception $e) {
    error_log('API Error: ' . $e->getMessage());
}

安全加固清单

风险项 防护措施
日志泄露 打日志前用 Log::mask($secret) 脱敏
错误泄露 异常消息覆盖为通用错误
令牌过期 实现自动刷新逻辑并处理401
网络攻击 启用 TLS 1.2+,校验证书
内存泄露 用后 unset()sodium_memzero()
密钥轮换 设计为可配置,支持无缝切换
// 安全内存清理示例
function clearSecret(string &$secret): void {
    // 使用 PHP 8.2+ 安全内存清零
    if (function_exists('sodium_memzero')) {
        sodium_memzero($secret);
    }
    $secret = '';
}

API 密钥简单场景

class ApiKeyClient {
    private string $apiKey;
    public function __construct() {
        $this->apiKey = getenv('API_KEY');
        if (!$this->apiKey) {
            throw new RuntimeException('API_KEY environment variable not set');
        }
    }
    public function call(string $url): array {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['X-API-KEY: ' . $this->apiKey],
            CURLOPT_SSL_VERIFYPEER => true
        ]);
        // ...
    }
}

项目结构建议

project/
├── config/
│   └── credentials.php       # 环境变量读取
├── src/
│   ├── Auth/
│   │   ├── OAuthClient.php  # 令牌管理
│   │   └── TokenStorage.php # 令牌持久化(可选 Redis)
│   └── Services/
│       └── ApiClient.php    # 具体业务 API
└── .env                     # 包含环境变量(不入库)

高级:Token 持久化到 Redis

class RedisTokenStorage {
    private Redis $redis;
    private string $keyPrefix = 'oauth:token:';
    public function __construct(Redis $redis) {
        $this->redis = $redis;
    }
    public function save(string $clientId, array $token): void {
        $this->redis->setex(
            $this->keyPrefix . $clientId,
            $token['expires_in'],
            json_encode($token)
        );
    }
    public function load(string $clientId): ?array {
        $data = $this->redis->get($this->keyPrefix . $clientId);
        return $data ? json_decode($data, true) : null;
    }
    public function clear(string $clientId): void {
        $this->redis->del($this->keyPrefix . $clientId);
    }
}

测试建议

// 使用 Mockery 测试时避免真实调用
public function testGetAccessToken(): void {
    $config = [
        'client_id' => 'test-id',
        'client_secret' => 'test-secret',
        'token_endpoint' => 'https://example.com/token'
    ];
    $client = Mockery::mock(OAuthClient::class);
    $client->shouldReceive('getAccessToken')
           ->andReturn('fake-token');
    $this->assertEquals('fake-token', $client->getAccessToken());
}

凭证管理的核心在于 分离加密

  • 分离:凭证与代码分离,通过环境变量/外部服务管理
  • 加密:传输加密(HTTPS)、存储加密(如需要)
  • 不记录:绝不把秘密写入日志
  • 及时刷新:令牌过期自动处理
  • 最小权限:使用最小必要 scope

这能确保你的 PHP 应用在凭证管理方面既安全又符合生产标准。

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