VolcEngineService.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <?php
  2. namespace App\Services;
  3. use GuzzleHttp\Client;
  4. use GuzzleHttp\Exception\GuzzleException;
  5. class VolcEngineService
  6. {
  7. /** @var Client */
  8. protected $client;
  9. public function __construct()
  10. {
  11. $this->client = new Client([
  12. 'timeout' => 300,
  13. ]);
  14. }
  15. /**
  16. * 通用请求方法:内部完成火山引擎签名并发送 HTTP 请求
  17. *
  18. * @param string $method HTTP 方法:GET / POST / PUT ...
  19. * @param string $host 目标 Host,例如:iam.volcengineapi.com
  20. * @param string $path 请求路径,例如:"/" 或 "/"
  21. * @param array $query 查询参数(不含 Action/Version)
  22. * @param string $body 请求 Body 字符串(GET 一般为空字符串)
  23. * @param string $ak AccessKeyId
  24. * @param string $sk SecretAccessKey
  25. * @param string $service 服务名,例如:iam / vod / sms 等
  26. * @param string $region 区域,例如:cn-north-1
  27. * @param string $action 公共参数 Action
  28. * @param string $version 公共参数 Version
  29. * @param string $contentType Content-Type,默认 application/json
  30. *
  31. * @return array [
  32. * 'status' => int,
  33. * 'headers' => array,
  34. * 'body' => string,
  35. * ]
  36. * @throws GuzzleException
  37. */
  38. public function request(
  39. string $method,
  40. string $host,
  41. string $path,
  42. array $query,
  43. string $body,
  44. string $ak,
  45. string $sk,
  46. string $service,
  47. string $region,
  48. string $action,
  49. string $version,
  50. string $contentType = 'application/json'
  51. ): array {
  52. $signed = $this->sign(
  53. $method,
  54. $host,
  55. $path,
  56. $query,
  57. $body,
  58. $ak,
  59. $sk,
  60. $service,
  61. $region,
  62. $action,
  63. $version,
  64. $contentType
  65. );
  66. $response = $this->client->request($method, 'https://' . $host . $path, [
  67. 'headers' => $signed['headers'],
  68. 'query' => $signed['query'],
  69. 'body' => $body,
  70. ]);
  71. return [
  72. 'status' => $response->getStatusCode(),
  73. 'headers' => $response->getHeaders(),
  74. 'body' => (string) $response->getBody(),
  75. ];
  76. }
  77. /**
  78. * 生成火山引擎签名头部和 query
  79. *
  80. * @return array [
  81. * 'headers' => [...], // 需要加到 HTTP Header 的字段
  82. * 'query' => [...], // 已合并 Action/Version 并排序后的 query
  83. * ]
  84. */
  85. public function sign(
  86. string $method,
  87. string $host,
  88. string $path,
  89. array $query,
  90. string $body,
  91. string $ak,
  92. string $sk,
  93. string $service,
  94. string $region,
  95. string $action,
  96. string $version,
  97. string $contentType = 'application/json'
  98. ): array {
  99. // 1. 合并公共参数,并按 key 排序
  100. $query = array_merge($query, [
  101. 'Action' => $action,
  102. 'Version' => $version,
  103. ]);
  104. ksort($query);
  105. // 2. 时间与 Body 哈希
  106. $xDate = gmdate('Ymd\THis\Z'); // 例如 20250105T081030Z
  107. $shortDate = substr($xDate, 0, 8); // 例如 20250105
  108. $xContentSha256 = hash('sha256', $body);
  109. // 3. CanonicalRequest
  110. $signedHeaders = 'content-type;host;x-content-sha256;x-date';
  111. $canonicalHeaders = implode("\n", [
  112. 'content-type:' . $contentType,
  113. 'host:' . $host,
  114. 'x-content-sha256:' . $xContentSha256,
  115. 'x-date:' . $xDate,
  116. ]);
  117. $canonicalQueryString = http_build_query($query);
  118. $canonicalRequest = implode("\n", [
  119. strtoupper($method),
  120. $path,
  121. $canonicalQueryString,
  122. $canonicalHeaders,
  123. '',
  124. $signedHeaders,
  125. $xContentSha256,
  126. ]);
  127. $hashedCanonicalRequest = hash('sha256', $canonicalRequest);
  128. // 4. StringToSign
  129. $credentialScope = implode('/', [$shortDate, $region, $service, 'request']);
  130. $stringToSign = implode("\n", [
  131. 'HMAC-SHA256',
  132. $xDate,
  133. $credentialScope,
  134. $hashedCanonicalRequest,
  135. ]);
  136. // 5. 派生签名 key
  137. $kDate = hash_hmac('sha256', $shortDate, $sk, true);
  138. $kRegion = hash_hmac('sha256', $region, $kDate, true);
  139. $kService = hash_hmac('sha256', $service, $kRegion, true);
  140. $kSigning = hash_hmac('sha256', 'request', $kService, true);
  141. // 6. 计算最终签名
  142. $signature = hash_hmac('sha256', $stringToSign, $kSigning);
  143. $authorization = sprintf(
  144. 'HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s',
  145. $ak . '/' . $credentialScope,
  146. $signedHeaders,
  147. $signature
  148. );
  149. $headers = [
  150. 'Host' => $host,
  151. 'Content-Type' => $contentType,
  152. 'X-Content-Sha256' => $xContentSha256,
  153. 'X-Date' => $xDate,
  154. 'Authorization' => $authorization,
  155. ];
  156. return [
  157. 'headers' => $headers,
  158. 'query' => $query,
  159. ];
  160. }
  161. }