正规第三方api不需应用层密钥协商,只需https+预置密钥签名;php用hash_hmac生成hmac-sha256签名时须严格按文档拼参、urlencode并注意参数顺序与编码格式。

PHP调用第三方API时密钥协商通常不走TLS层外的自定义流程
绝大多数正规第三方API(如微信支付、支付宝、Stripe、AWS)**不让你自己实现密钥协商**,而是直接要求你使用 HTTPS + 预置的 app_id 和 secret_key(或 access_token),签名基于已知密钥生成。所谓“加密协商密钥”是 TLS 握手的事,PHP 的 curl 或 file_get_contents 走 HTTPS 时自动完成,你不需要、也不应该在应用层再做一次 DH/ECDH 协商。
真正要你做的,是:拿到平台分配的密钥对(client_id + client_secret 或 api_key + api_secret),按文档要求对请求参数排序、拼接、哈希(常见 sha256、hmac_sha256),再把签名塞进 Header 或 Query。
- 别试图用
openssl_pkey_new()手动协商密钥——API 服务端根本不会配合 - 别把
client_secret当成 AES 密钥去加密整个 body——99% 的 API 不接受密文 body,只校验签名 - 时间戳、随机字符串(
nonce)、签名算法标识(sign_method)往往是签名必需字段,漏掉就401 Unauthorized
PHP生成HMAC-SHA256签名的典型写法和易错点
签名本质是「用密钥对标准化后的请求数据做不可逆摘要」,不是加密。最常用的是 hash_hmac(),但要注意参数顺序和编码:
-
hash_hmac('sha256', $message, $secret_key, true)第三个参数是密钥,第四个true表示返回原始二进制,需base64_encode()或bin2hex()转成字符串(看 API 要求) -
$message必须严格按文档拼接:通常是参数按字典序排序后,用&连接,=左右不加空格,且所有值必须urlencode()(注意:不是rawurlencode(),有些 API 明确要求空格转+) - 常见错误:
file_get_contents()发送 POST 时没设Content-Type: application/x-www-form-urlencoded,导致服务端解析参数失败,签名原文跟服务端算的不一致
示例(微信公众号获取 access_token 后的签名):
立即学习“PHP免费学习笔记(深入)”;
```php
$params = [
'appid' => 'wx123',
'secret' => 'abc456',
'timestamp' => time(),
'noncestr' => bin2hex(random_bytes(8)),
];
ksort($params);
$message = http_build_query($params, '', '&', PHP_QUERY_RFC3986); // 注意 RFC3986 编码风格
$signature = hash_hmac('sha256', $message, $api_secret, true);
$sign = base64_encode($signature);
```cURL发送带签名请求时Header和Body的匹配陷阱
签名是否生效,取决于你签名时用的「原始请求数据」和服务端收到并复现的数据是否完全一致。cURL 默认行为容易破坏一致性:
- 如果 API 要求签名覆盖
Content-Type,你必须把它写进签名原文,并在curl_setopt($ch, CURLOPT_HTTPHEADER, [...])中显式设置,不能依赖 cURL 自动推断 - 用
json_encode()构造 body 后直接发,但签名时却按application/x-www-form-urlencoded规则拼参数 → 签名无效 -
curl_setopt($ch, CURLOPT_POSTFIELDS, $data)中$data是数组时,cURL 会自动转成 form-data;是字符串时才原样发送。务必确认你签名用的$message和实际发出的 body 字符串完全等价 - 某些 API(如阿里云 OpenAPI)要求签名中包含
Host、X-Date等 header 字段,漏掉任一 header 就拒绝
验证第三方响应签名时要注意公钥格式和填充方式
少数 API(如苹果 App Store Server Notifications、部分银行接口)会返回带签名的 JSON 响应,要求你用其公钥验签。这时 PHP 的 openssl_verify() 是主力,但坑很多:
- 公钥必须是 PEM 格式(以
-----BEGIN PUBLIC KEY-----开头),如果给的是 JWK 或 DER,得先转换:openssl_pkey_get_public("file://path/to/pubkey.pem")才能用 - 验签前必须从响应里完整提取出原始 payload(不含 signature 字段)和 base64url 解码后的 signature,且 payload 必须跟签名时服务端拼的**一字不差**(包括换行、空格、JSON 键序)
- 算法要匹配:服务端若用
RS256(RSA + SHA256),PHP 得用OPENSSL_ALGO_SHA256;若用ES256(ECDSA),则需openssl_pkey_get_public()支持 EC key,PHP ≥ 7.1 - 别用
hash_equals()对比签名结果——那是给 HMAC 设计的,RSA/ECDSA 验签必须用openssl_verify()
验签失败,90% 的原因是 payload 预处理不一致,而不是密钥或算法错了。











