appId === '' || $this->appSecret === '') { throw new InvalidArgumentException('E 签宝 App ID 和 AppSecret 不能为空。'); } } public function headers(string $method, string $pathAndParameters, string $body = ''): array { $method = strtoupper($method); $pathAndParameters = $this->canonicalPath($pathAndParameters); $contentMd5 = $body === '' ? '' : base64_encode(md5($body, true)); $stringToSign = implode("\n", [ $method, self::ACCEPT, $contentMd5, self::CONTENT_TYPE, '', ]) . "\n" . $pathAndParameters; return [ 'X-Tsign-Open-App-Id' => $this->appId, 'X-Tsign-Open-Auth-Mode' => 'Signature', 'X-Tsign-Open-Ca-Timestamp' => (string) floor(microtime(true) * 1000), 'X-Tsign-Open-Ca-Signature' => base64_encode(hash_hmac('sha256', $stringToSign, $this->appSecret, true)), 'Accept' => self::ACCEPT, 'Content-Type' => self::CONTENT_TYPE, 'Content-MD5' => $contentMd5, ]; } public function canonicalPath(string $pathAndParameters): string { $parts = parse_url($pathAndParameters); $path = (string) ($parts['path'] ?? ''); if ($path === '' || ! str_starts_with($path, '/')) { throw new InvalidArgumentException('E 签宝 API 路径必须以 / 开头。'); } if (! isset($parts['query']) || $parts['query'] === '') { return $path; } $parameters = []; foreach (explode('&', $parts['query']) as $pair) { [$key, $value] = array_pad(explode('=', $pair, 2), 2, null); if (! array_key_exists($key, $parameters)) { $parameters[$key] = $value; } } ksort($parameters, SORT_STRING); $query = collect($parameters)->map(function (?string $value, string $key): string { return $value === null || $value === '' ? $key : $key . '=' . $value; })->implode('&'); return $path . '?' . $query; } }