ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP 8.4怎么实现API接口数据加密传输

PHP 8.4怎么实现API接口数据加密传输 前言先澄清一个常见误解把请求体加密并不等于「传输安全」。HTTPSTLS解决的恰恰就是传输安全——防窃听、防篡改、防中间人。如果只做应用层加密却不上 HTTPS中间人完全可以降级、替换公钥、重放到你的接口。那应用层再加密一层有什么用在真实项目里它解决的是 TLS 管不到的地方链路终点之后的明文TLS 在网关处终止之后再转发的一段通常是明文日志系统、APM、反向代理都可能记录完整报文。字段级合规要求某些行业规范要求敏感字段证件号、银行卡、健康数据在「报文层面」就必须是密文而不是只依赖链路。跨系统对账留痕加密信封能证明某个字段在传输过程中没有被任何中间环节改写。本文讲的是信封加密envelope encryption用 AES-256-GCM 加密业务数据再用接收方的 RSA 公钥包裹这把临时对称密钥。它的好处是既能加密大报文对称算法快又不需要在发送方存放对称密钥只有接收方能解开。关于版本标题里的 PHP 8.4 指的是运行环境版本不是加密能力的引入版本。openssl_*系列函数和sodium_*系列函数早在 8.4 之前就存在加密算法本身与 PHP 版本无关8.4 带来的是写起来更舒服的语法——属性钩子property hooks、非对称可见性asymmetric visibility、array_find()。文中代码最低要求PHP 8.4。一、方案选型三种做法只有一种该用方案用什么密钥管理适用场景评价只签名不加密hash_hmac(sha256, ...)双方共享一个密钥防篡改不要求保密需要「可验证但不必保密」时够用纯对称加密openssl_encrypt 共享密钥双方共享同一个 AES 密钥系统间点对点、运维可控密钥分发是弱点成员一多就失控信封加密AES-256-GCM RSA-OAEP发送方只有公钥面向不特定客户端、字段级加密本文主推信封加密的密钥分发是单向的接收方公布公钥私钥不出服务器。发送方每次请求临时生成一把对称密钥用完即弃。即使公钥被替换攻击者也无法解密——但前提是你通过可信渠道获取公钥这也是纯应用层加密无法摆脱的信任起点问题所以公钥要么硬编码在客户端要么由已受 TLS 保护的接口下发。还要明确一点GCM 已经自带完整性认证AEADauthenticated encryption with associated data解密时如果密文被改过openssl_decrypt会直接返回false所以不需要「先加密再额外 HMAC 一遍」如果一定要额外加签名绝不能复用同一把密钥。二、PHP 8.4 里写起来更顺的部分三处 8.4 的语法改进在这里特别顺手非对称可见性用来保护私钥外部只能读只有类自己内部的构造函数能写。属性钩子把「base64 解码」这一步藏进读属性的动作里调用方写$envelope-ciphertext拿到的就是解码后的二进制不必到处写base64_decode。?php declare(strict_types1); // 需要 PHP 8.4 final class KeyPair { // PHP 8.4 非对称可见性外部只读内部可写 public private(set) string $secretKey; // PHP 8.1 的 readonly 在这里正好 public readonly string $publicKey; public function __construct() { $res openssl_pkey_new([ private_key_bits 2048, private_key_type OPENSSL_KEYTYPE_RSA, ]); if ($res false) { throw new RuntimeException(生成密钥对失败); } if (!openssl_pkey_export($res, $priv)) { throw new RuntimeException(导出私钥失败); } $details openssl_pkey_get_details($res); if ($details false) { throw new RuntimeException(读取公钥失败); } $this-secretKey $priv; $this-publicKey $details[key]; } }三、完整可运行的信封加密实现下面这份代码可以直接保存为envelope_demo.php运行不需要任何 Composer 包只需要openssl扩展?php declare(strict_types1); // 需要 PHP 8.4扩展 openssl // 用法: php envelope_demo.php final class Sealed { public function __construct( public readonly string $ivB64, public readonly string $tagB64, public readonly string $ctB64, public readonly string $wrappedB64, ) {} public function toJson(): string { return json_encode([ v 1, alg AES-256-GCMRSA-OAEP, iv $this-ivB64, tag $this-tagB64, ct $this-ctB64, key $this-wrappedB64, ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); } } /** * PHP 8.4 属性钩子读的时候自动 base64 解码 * 解码失败直接抛异常而不是悄悄返回空串。 * 只带 get 钩子的属性是「虚属性」不占存储只能读不能写。 */ final class Envelope { public string $ciphertext { get base64_decode($this-ctB64, true) ?: throw new RuntimeException(密文不是合法 base64); } public string $nonce { get base64_decode($this-ivB64, true) ?: throw new RuntimeException(IV 不是合法 base64); } public string $tag { get base64_decode($this-tagB64, true) ?: throw new RuntimeException(tag 不是合法 base64); } public function __construct( private string $ctB64, private string $ivB64, private string $tagB64, ) {} } final class EnvelopeCrypto { private const CIPHER aes-256-gcm; private const IV_LEN 12; // GCM 推荐 96 bit private const AAD api-payload-v1; // 附加认证数据参与校验但不加密 /** 发送方加密 包裹密钥 */ public function seal(string $plaintext, string $publicKeyPem): string { $aesKey random_bytes(32); // 每次请求新生成绝不复用 $iv random_bytes(self::IV_LEN); // 每次请求新 IV绝不复用 $tag ; $ct openssl_encrypt( $plaintext, self::CIPHER, $aesKey, OPENSSL_RAW_DATA, $iv, $tag, self::AAD, 16 ); if ($ct false) { throw new RuntimeException(对称加密失败: . (openssl_error_string() ?: unknown)); } $wrapped ; if (!openssl_public_encrypt($aesKey, $wrapped, $publicKeyPem, OPENSSL_PKCS1_OAEP_PADDING)) { throw new RuntimeException(包裹密钥失败: . (openssl_error_string() ?: unknown)); } return (new Sealed( base64_encode($iv), base64_encode($tag), base64_encode($ct), base64_encode($wrapped), ))-toJson(); } /** 接收方解包密钥 认证解密 */ public function open(string $json, string $privateKeyPem): string { $data json_decode($json, true, 512, JSON_THROW_ON_ERROR); foreach ([iv, tag, ct, key] as $field) { if (!isset($data[$field]) || !is_string($data[$field])) { throw new RuntimeException(信封缺少字段: {$field}); } } $env new Envelope($data[ct], $data[iv], $data[tag]); $wrappedKey base64_decode($data[key], true); if ($wrappedKey false) { throw new RuntimeException(包裹密钥不是合法 base64); } $aesKey ; if (!openssl_private_decrypt($wrappedKey, $aesKey, $privateKeyPem, OPENSSL_PKCS1_OAEP_PADDING)) { throw new RuntimeException(解包密钥失败私钥不匹配或信封被改过); } $plain openssl_decrypt( $env-ciphertext, self::CIPHER, $aesKey, OPENSSL_RAW_DATA, $env-nonce, $env-tag, self::AAD ); if ($plain false) { // 走到这里通常意味着密文/tag/IV/AAD 任一项被改动 throw new RuntimeException(认证解密失败数据完整性问题); } return $plain; } } // ---------- 演示 ---------- $keys new KeyPair(); $crypt new EnvelopeCrypto(); $payload json_encode([ user_id 10086, id_card 11010119900307XXXX, amount 199.00, ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); $envelopeJson $crypt-seal($payload, $keys-publicKey); echo 信封可以直接放进请求体或 HTTP 头:\n; echo $envelopeJson . \n\n; $plain $crypt-open($envelopeJson, $keys-secretKey); echo 解密结果:\n . $plain . \n; echo $plain $payload ? 往返一致 ✅\n : 往返不一致 ❌\n; // 篡改演示改一个字符再解必须失败 $tampered substr_replace($envelopeJson, X, 60, 1); try { $crypt-open($tampered, $keys-secretKey); echo 篡改后仍能解密 ❌说明校验有问题\n; } catch (RuntimeException $e) { echo 篡改被拦截 ✅: . $e-getMessage() . \n; }需要说明的是openssl_public_encrypt用 OAEP 填充时单次能包裹的明文长度受密钥长度限制2048 位密钥下上限是两百多字节远大于 32 字节的 AES 密钥。不要用它去加密业务数据本身——业务数据必须走对称算法。如果服务器装了sodium扩展sodium_crypto_box_seal()/sodium_crypto_box_seal_open()提供匿名公钥加密发送方不需要自己的密钥对具体接口请以官方文档为准。四、防重放加密解决不了的第三件事信封加密保证了机密性和完整性但拦截下来的密文可以原样重放。所以还需要在信封外层加两个字段?php declare(strict_types1); final class ReplayGuard { /** 允许的时间窗口秒 */ public function __construct(private int $windowSec 300) {} public function build(array $payload, string $sharedSecret): array { $payload[_ts] time(); $payload[_nonce] bin2hex(random_bytes(16)); // 服务端需要用它去重 $payload[_sig] $this-sign($payload, $sharedSecret); return $payload; } public function verify(array $payload, string $sharedSecret): void { $sig $payload[_sig] ?? ; unset($payload[_sig]); // 1) 恒定时间比较避免时序侧信道 if (!hash_equals($this-sign($payload, $sharedSecret), (string) $sig)) { throw new RuntimeException(签名不匹配); } // 2) 时间窗口 $ts (int) ($payload[_ts] ?? 0); if (abs(time() - $ts) $this-windowSec) { throw new RuntimeException(时间戳超出允许窗口); } // 3) nonce 去重生产环境用 Redis SET NX 加 TTL // 这里用文件仅作演示 $seen sys_get_temp_dir() . /nonce_ . md5((string) $payload[_nonce]); if (file_exists($seen)) { throw new RuntimeException(nonce 已被使用疑似重放); } file_put_contents($seen, 1, LOCK_EX); } private function sign(array $payload, string $secret): string { ksort($payload); // 顺序必须稳定否则两端算出的签名不同 $data json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return hash_hmac(sha256, (string) $data, $secret); } }三点必须注意ksort保证两端字段顺序一致比较签名用hash_equals而不是nonce 的去重存储必须带过期时间否则这个集合会无限膨胀。常见坑点1. 认为「加密了就不用 HTTPS」❌ 内网接口用 HTTP靠应用层 AES 保证安全。 ✅ 底层永远先上 TLS应用层加密作为叠加的纵深防御。没有 TLS攻击者可以中间人替换公钥之后你加密的所有内容他都能解。应用层加密的前提是公钥获取渠道可信而这个可信度本身就建立在 TLS 上。2. 复用 IV❌ 用固定 IV或把 IV 设成str_repeat(0, 12)。 ✅ 每次加密都random_bytes(12)生成新 IV并随密文一起传输。GCM 模式下 IV 复用是灾难性的同一密钥下重复使用 IV 会导致密钥流重叠攻击者可以通过异或两条密文恢复明文甚至可以伪造认证标签。这是 AES-GCM 最严重的误用。3. 用比较签名❌if ($computed $received) { /* 通过 */ }✅if (hash_equals($computed, $received)) { /* 通过 */ }普通字符串比较遇到第一个不同字符就返回攻击者可以通过测量响应时间逐字节猜出签名。hash_equals是恒定时间比较。4. 加密密钥和认证密钥共用一把❌ 用同一个密钥既做 AES 加密又做 HMAC。 ✅ 两者的密钥必须独立派生。在 GCM 场景下这条其实是多余的——GCM 已提供认证。如果要额外签名用不同密钥。同密钥叠加加密与认证在密码学上是明确的错误用法。5. 私钥放在 Web 可访问目录❌ 把private.pem放在public/或项目根目录。 ✅ 放在 Web 根目录之外文件权限设成仅服务账户可读。放在 Web 根下的.pem很容易因为没有 Nginx 的location ~ \.pem$拦截规则而被直接下载。这类泄露往往很久都发现不了。6. 把解密后的明文写进日志❌ 在中间件里记录解密后的完整请求体方便排查问题。 ✅ 只记录字段名、长度、业务主键敏感字段打码。应用层加密的初衷就是躲开日志系统的明文残留结果自己又把它记了一份这个方案就白做了。7. 不检查openssl_encrypt的返回值❌$ct openssl_encrypt(...); return base64_encode($ct);✅ 检查 false并把openssl_error_string()记进日志。加密失败会返回falsebase64_encode(false)会得到空串于是你把一个空信封发出去接收方报「解密失败」排查方向完全被带偏。总结环节该用什么不该用什么关键点链路安全TLS只靠应用层加密应用层加密不能替代 HTTPS数据加密AES-256-GCMECB、复用的 IVIV 每次必须随机密钥传递RSA-OAEP 包裹临时密钥双方硬编码同一 AES 密钥私钥不出服务器完整性GCM 的认证标签MD5、明文拼接做摘要加密与认证密钥要独立签名比较hash_equals/防时序侧信道防重放时间戳 nonce TTL只靠加密密文本身可以原样重放日志字段名与长度解密后的明文别自己制造明文残留实现 API 数据加密传输的正确姿势是「TLS 信封加密 防重放」三层叠加TLS 保证链路AES-256-GCM 保证报文的机密性与完整性时间戳加 nonce 保证请求不能被重放。三者缺一方案都会在某个具体场景下失效。PHP 8.4 在这里的角色是让代码更好写——非对称可见性保护私钥、属性钩子把解码逻辑收进属性——但加密算法与 PHP 版本无关把项目升到 8.4 并不会让加密「更安全」安全来自上面这三层的正确组合。
返回列表