PHP回调解密怎么保证安全

wen PHP项目 1

本文目录导读:

PHP回调解密怎么保证安全

  1. 加密算法的选择
  2. 密钥管理(关键)
  3. 签名验证(推荐)
  4. 防止常见攻击
  5. 完整的安全实现示例
  6. 安全清单

在PHP中实现安全的回调解密,核心原则是假设回调数据可被攻击者完全控制,以下是关键的安全措施和最佳实践:

加密算法的选择

// 推荐:AES-256-GCM(带认证的加密)
// 不推荐:ECB模式(无IV,不安全)、CBC不带MAC(易受填充预言攻击)
class SecureCallbackHandler {
    private string $key;
    public function __construct(string $encryptionKey) {
        $this->key = hash('sha256', $encryptionKey, true); // 确保32字节
    }
    public function decrypt(string $payload): array {
        try {
            $data = json_decode($payload, true);
            // 必需字段验证
            if (!isset($data['ciphertext']) || !isset($data['iv']) || 
                !isset($data['tag']) || !isset($data['timestamp'])) {
                throw new \Exception('Invalid payload structure');
            }
            // 时间戳验证(防重放攻击)
            if (abs(time() - $data['timestamp']) > 300) { // 5分钟有效期
                throw new \Exception('Payload expired');
            }
            // 解密 + 认证
            $plaintext = openssl_decrypt(
                base64_decode($data['ciphertext']),
                'aes-256-gcm',
                $this->key,
                OPENSSL_RAW_DATA,
                base64_decode($data['iv']),
                base64_decode($data['tag'])
            );
            if ($plaintext === false) {
                throw new \Exception('Decryption failed');
            }
            // 解析数据
            $result = json_decode($plaintext, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Invalid JSON data');
            }
            return $result;
        } catch (\Exception $e) {
            error_log("Callback decryption failed: " . $e->getMessage());
            throw new \Exception('Invalid callback data');
        }
    }
}

密钥管理(关键)

// 密钥不能硬编码在代码中
// 应从环境变量或密钥管理系统获取
class KeyManager {
    public static function getCallbackKey(): string {
        // 方式1:环境变量
        $key = getenv('CALLBACK_ENCRYPTION_KEY');
        if (!$key) {
            throw new \RuntimeException('Callback key not configured');
        }
        // 方式2:密钥轮换支持
        // 使用多版本密钥:
        // $keys = json_decode(getenv('CALLBACK_KEYS'), true);
        // return $keys['current'];
        return $key;
    }
    public static function rotateKey(): void {
        // 实现密钥轮换逻辑
        // 1. 生成新密钥
        // 2. 存储历史密钥用于解密旧数据
        // 3. 更新环境变量/密钥库
    }
}

签名验证(推荐)

class CallbackSecurity {
    private string $secretKey;
    public function __construct(string $secretKey) {
        $this->secretKey = $secretKey;
    }
    public function verifyAndDecrypt(array $rawData): array {
        // 方式1:HMAC签名验证(推荐)
        $receivedSignature = $rawData['signature'] ?? '';
        unset($rawData['signature']);
        // 规范化数据(按key排序)
        ksort($rawData);
        $payload = json_encode($rawData);
        $expectedSignature = hash_hmac('sha256', $payload, $this->secretKey);
        // 使用hash_equals防止时序攻击
        if (!hash_equals($expectedSignature, $receivedSignature)) {
            throw new \Exception('Signature mismatch - possible tampering');
        }
        // 方式2:RSA非对称签名
        /*
        $publicKey = openssl_pkey_get_public(file_get_contents('rsa_public.pem'));
        $verified = openssl_verify(
            $rawData['data'],
            base64_decode($rawData['signature']),
            $publicKey,
            OPENSSL_ALGO_SHA256
        );
        if ($verified !== 1) {
            throw new \Exception('RSA signature verification failed');
        }
        */
        // 签名通过后,再进行解密
        return $this->decryptPayload($rawData['data']);
    }
}

防止常见攻击

class SecureCallbackProcessor {
    // 反重放攻击
    private array $replayCache = [];
    public function processPayments(array $encryptedData): array {
        // 1. 防重放(使用唯一ID)
        if (isset($encryptedData['request_id'])) {
            $requestId = $encryptedData['request_id'];
            // 使用Redis等存储已处理过的request_id
            if ($this->isRequestDuplicate($requestId)) {
                throw new \Exception('Duplicate callback request');
            }
            $this->storeRequestId($requestId);
        }
        // 2. 防篡改(数据完整性)
        $data = $this->decryptAndVerify($encryptedData);
        // 3. 参数校验
        $this->validateBusinessLogic($data);
        // 4. 幂等处理(防止重复执行)
        $this->processIdempotently($data);
        return $data;
    }
    // 防时序攻击
    private function secureComparison(string $a, string $b): bool {
        return hash_equals($a, $b); // PHP内置的时序安全比较
    }
    // 日志脱敏
    private function logSafely(array $data): void {
        // 不要记录敏感字段
        unset($data['card_number'], $data['password'], $data['token']);
        error_log(json_encode($data));
    }
}

完整的安全实现示例

<?php
class SecureCallbackHandler {
    private string $encryptionKey;
    private string $signingSecret;
    private \Redis $redis; // 用于重放防护
    public function __construct(string $encryptionKey, string $signingSecret, \Redis $redis) {
        $this->encryptionKey = $encryptionKey;
        $this->signingSecret = $signingSecret;
        $this->redis = $redis;
    }
    public function handle(string $rawPayload): array {
        try {
            // Step 1: 基础JSON解析
            $payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
            // Step 2: 结构验证
            $this->validateStructure($payload);
            // Step 3: 签名验证(确保数据完整性)
            $this->verifySignature($payload);
            // Step 4: 防重放检查
            $this->checkReplay($payload['request_id']);
            // Step 5: 解密数据
            $decryptedData = $this->decrypt(
                base64_decode($payload['data']),
                base64_decode($payload['iv']),
                base64_decode($payload['tag'])
            );
            // Step 6: 业务逻辑验证
            $this->validateBusinessData($decryptedData);
            // Step 7: 标记已处理(防重放)
            $this->markAsProcessed($payload['request_id']);
            return $decryptedData;
        } catch (\Throwable $e) {
            // 安全日志,不暴露敏感信息
            error_log(sprintf(
                'Callback failed: %s, IP: %s',
                $e->getMessage(),
                $_SERVER['REMOTE_ADDR'] ?? 'unknown'
            ));
            throw $e;
        }
    }
    private function validateStructure(array $payload): void {
        $required = ['request_id', 'timestamp', 'signature', 'data', 'iv', 'tag'];
        foreach ($required as $field) {
            if (!isset($payload[$field])) {
                throw new \InvalidArgumentException("Missing field: $field");
            }
        }
        // 时间窗口验证(5分钟)
        if (abs(time() - $payload['timestamp']) > 300) {
            throw new \Exception('Timestamp out of range');
        }
    }
    private function verifySignature(array $payload): void {
        // 规范化要签名的数据
        $signData = [
            'request_id' => $payload['request_id'],
            'timestamp' => $payload['timestamp'],
            'data' => $payload['data'],
            'iv' => $payload['iv'],
            'tag' => $payload['tag']
        ];
        ksort($signData);
        $stringToSign = json_encode($signData);
        $expectedSig = hash_hmac('sha256', $stringToSign, $this->signingSecret);
        if (!hash_equals($expectedSig, $payload['signature'])) {
            throw new \Exception('Invalid signature');
        }
    }
    private function decrypt(string $ciphertext, string $iv, string $tag): array {
        $plaintext = openssl_decrypt(
            $ciphertext,
            'aes-256-gcm',
            $this->encryptionKey,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );
        if ($plaintext === false) {
            throw new \Exception('Decryption failed');
        }
        $data = json_decode($plaintext, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid decrypted data');
        }
        return $data;
    }
    private function checkReplay(string $requestId): void {
        $key = "callback:processed:{$requestId}";
        if ($this->redis->exists($key)) {
            throw new \Exception('Duplicate request');
        }
    }
    private function markAsProcessed(string $requestId): void {
        $key = "callback:processed:{$requestId}";
        $this->redis->setex($key, 3600, '1'); // 1小时过期
    }
    private function validateBusinessData(array $data): void {
        // 根据业务需要验证数据
        if (!isset($data['order_id']) || !is_string($data['order_id'])) {
            throw new \InvalidArgumentException('Invalid order data');
        }
        // 数据范围验证
        if (isset($data['amount'])) {
            $amount = floatval($data['amount']);
            if ($amount <= 0 || $amount > 1000000) {
                throw new \InvalidArgumentException('Invalid amount');
            }
        }
    }
}
// 使用示例
$secureHandler = new SecureCallbackHandler(
    getenv('CALLBACK_ENCRYPTION_KEY'),
    getenv('CALLBACK_SIGNING_SECRET'),
    new \Redis()
);
// 接收回调
$rawData = file_get_contents('php://input');
$cleanData = $secureHandler->handle($rawData);

安全清单

必须实现

  • 使用 AES-256-GCMChaCha20-Poly1305 认证加密
  • 额外的 HMAC-SHA256 签名
  • 时间戳有效期(如5分钟)
  • 唯一request_id防重放
  • 使用 hash_equals() 进行安全比较
  • 密钥存储在环境变量/密钥管理服务

避免

  • 不要使用简单Base64编码作为"加密"
  • 不要使用可预测的IV(应该随机生成)
  • 不要在日志中记录敏感数据
  • 不要固定使用单个密钥(定期轮换)
  • 不要信任任何未经验证的输入

通过以上措施,可以建立一个多层次的防护体系,确保回调数据在传输和处理过程中的安全性。

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