|
|
@@ -0,0 +1,181 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Services;
|
|
|
+
|
|
|
+use GuzzleHttp\Client;
|
|
|
+use GuzzleHttp\Exception\GuzzleException;
|
|
|
+
|
|
|
+class VolcEngineService
|
|
|
+{
|
|
|
+ /** @var Client */
|
|
|
+ protected $client;
|
|
|
+
|
|
|
+ public function __construct()
|
|
|
+ {
|
|
|
+ $this->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,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+}
|