client = new Client([ 'timeout' => 300, ]); } /** * 通用请求方法:内部完成火山引擎签名并发送 HTTP 请求 * * @param string $method HTTP 方法:GET / POST / PUT ... * @param string $host 目标 Host,例如:iam.volcengineapi.com * @param string $path 请求路径,例如:"/" 或 "/" * @param array $query 查询参数(不含 Action/Version) * @param string $body 请求 Body 字符串(GET 一般为空字符串) * @param string $ak AccessKeyId * @param string $sk SecretAccessKey * @param string $service 服务名,例如:iam / vod / sms 等 * @param string $region 区域,例如:cn-north-1 * @param string $action 公共参数 Action * @param string $version 公共参数 Version * @param string $contentType Content-Type,默认 application/json * * @return array [ * 'status' => int, * 'headers' => array, * 'body' => string, * ] * @throws GuzzleException */ public function request( string $method, string $host, string $path, array $query, string $body, string $ak, string $sk, string $service, string $region, string $action, string $version, string $contentType = 'application/json' ): array { $signed = $this->sign( $method, $host, $path, $query, $body, $ak, $sk, $service, $region, $action, $version, $contentType ); $response = $this->client->request($method, 'https://' . $host . $path, [ 'headers' => $signed['headers'], 'query' => $signed['query'], 'body' => $body, ]); return [ 'status' => $response->getStatusCode(), 'headers' => $response->getHeaders(), 'body' => (string) $response->getBody(), ]; } /** * 生成火山引擎签名头部和 query * * @return array [ * 'headers' => [...], // 需要加到 HTTP Header 的字段 * 'query' => [...], // 已合并 Action/Version 并排序后的 query * ] */ public function sign( string $method, string $host, string $path, array $query, string $body, string $ak, string $sk, string $service, string $region, string $action, string $version, string $contentType = 'application/json' ): array { // 1. 合并公共参数,并按 key 排序 $query = array_merge($query, [ 'Action' => $action, 'Version' => $version, ]); ksort($query); // 2. 时间与 Body 哈希 $xDate = gmdate('Ymd\THis\Z'); // 例如 20250105T081030Z $shortDate = substr($xDate, 0, 8); // 例如 20250105 $xContentSha256 = hash('sha256', $body); // 3. CanonicalRequest $signedHeaders = 'content-type;host;x-content-sha256;x-date'; $canonicalHeaders = implode("\n", [ 'content-type:' . $contentType, 'host:' . $host, 'x-content-sha256:' . $xContentSha256, 'x-date:' . $xDate, ]); $canonicalQueryString = http_build_query($query); $canonicalRequest = implode("\n", [ strtoupper($method), $path, $canonicalQueryString, $canonicalHeaders, '', $signedHeaders, $xContentSha256, ]); $hashedCanonicalRequest = hash('sha256', $canonicalRequest); // 4. StringToSign $credentialScope = implode('/', [$shortDate, $region, $service, 'request']); $stringToSign = implode("\n", [ 'HMAC-SHA256', $xDate, $credentialScope, $hashedCanonicalRequest, ]); // 5. 派生签名 key $kDate = hash_hmac('sha256', $shortDate, $sk, true); $kRegion = hash_hmac('sha256', $region, $kDate, true); $kService = hash_hmac('sha256', $service, $kRegion, true); $kSigning = hash_hmac('sha256', 'request', $kService, true); // 6. 计算最终签名 $signature = hash_hmac('sha256', $stringToSign, $kSigning); $authorization = sprintf( 'HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s', $ak . '/' . $credentialScope, $signedHeaders, $signature ); $headers = [ 'Host' => $host, 'Content-Type' => $contentType, 'X-Content-Sha256' => $xContentSha256, 'X-Date' => $xDate, 'Authorization' => $authorization, ]; return [ 'headers' => $headers, 'query' => $query, ]; } }