url = rtrim($url, '/'); $this->ak = $ak; $this->sk = $sk; $this->userAgent = $userAgent; } /** * @param array $payload 已含 package、class 及业务字段 * @return array{state:int,msg:string,body?:array,response?:string} */ public function post(array $payload): array { // 与甘草网关规范一致:签名/头里的时间戳须与 body 内业务字段 timestamp(若有)一致,避免跨秒不一致导致签名校验失败 $sigTs = time(); if (isset($payload['timestamp']) && is_numeric($payload['timestamp'])) { $sigTs = (int) $payload['timestamp']; } if ($sigTs <= 0) { $sigTs = time(); } $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { return ['state' => 0, 'msg' => 'json_encode 失败']; } $noise = self::randStr(8); $signature = sha1($json . $sigTs . $noise . $this->sk); $cipher = self::encrypt($json, $this->sk); if ($cipher === '') { return ['state' => 0, 'msg' => 'AES 加密失败']; } $headers = [ 'Connection: close', 'Content-Type: application/json; charset=utf-8', 'Content-length: ' . strlen($cipher), 'Cache-Control: no-cache', 'AK: ' . $this->ak, 'Signature: ' . $signature, 'UTC-Timestamp: ' . $sigTs, 'NOISE: ' . $noise, 'Expect:', ]; // cURL 常量在部分精简构建里可能未注册,统一用 defined() 做优雅降级,值取自官方枚举 $httpVer11 = defined('CURL_HTTP_VERSION_1_1') ? CURL_HTTP_VERSION_1_1 : 2; $ipv4Only = defined('CURL_IPRESOLVE_V4') ? CURL_IPRESOLVE_V4 : 1; $ch = curl_init($this->url); curl_setopt($ch, CURLOPT_HTTP_VERSION, $httpVer11); // 改用 HTTP/1.1 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_ENCODING, 'gzip'); curl_setopt($ch, CURLOPT_IPRESOLVE, $ipv4Only); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 增加连接超时 curl_setopt($ch, CURLOPT_TIMEOUT, 120); curl_setopt($ch, CURLOPT_POSTFIELDS, $cipher); curl_setopt($ch, CURLOPT_HEADER, true); if (str_starts_with($this->url, 'https:')) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 改为 0,完全禁用主机验证 // 强制 TLS 1.2;常量值为 6,但部分 PHP/cURL 构建未注册该常量,运行时未定义时退化为默认 TLS $tls12 = defined('CURL_SSLVERSION_TLSv1_2') ? CURL_SSLVERSION_TLSv1_2 : 6; curl_setopt($ch, CURLOPT_SSLVERSION, $tls12); // 可选 cipher list。默认不设(交给 cURL 自带默认,兼容性最好)。 // 需要兼容弱加密服务器时,在 config/gancao_scm.php 或 .env 里设: // GANCAO_SCM_TLS_CIPHERS="DEFAULT@SECLEVEL=1" // 仅 OpenSSL 构建的 cURL 支持此语法 // GANCAO_SCM_TLS_CIPHERS="DEFAULT:!aNULL:!eNULL" // 通用写法 // cURL 是 LibreSSL/NSS/GnuTLS/BoringSSL 时,@SECLEVEL= 会直接报 CURLE_SSL_CIPHER(59)。 $cipherList = trim((string) \think\facade\Config::get('gancao_scm.tls_ciphers', '')); if ($cipherList !== '') { curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, $cipherList); } } // 添加 TCP keepalive(部分旧 libcurl 可能没注册 CURLOPT_TCP_KEEPALIVE,守一下) if (defined('CURLOPT_TCP_KEEPALIVE')) { curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1); curl_setopt($ch, CURLOPT_TCP_KEEPIDLE, 120); curl_setopt($ch, CURLOPT_TCP_KEEPINTVL, 60); } $raw = curl_exec($ch); $info = curl_getinfo($ch); $curlErr = curl_errno($ch); $curlMsg = curl_error($ch); curl_close($ch); if ($raw === false || (int) ($info['http_code'] ?? 0) !== 200) { $http = (int) ($info['http_code'] ?? 0); $err = is_string($raw) ? $raw : ''; return [ 'state' => 0, 'msg' => '通信失败:HTTP ' . $http . ($curlErr !== 0 ? ' curl#' . $curlErr . ' ' . $curlMsg : ''), 'response' => $err, ]; } $bodyRaw = substr($raw, strpos($raw, "\r\n\r\n") + 4); $plain = self::decrypt($bodyRaw, $this->sk); if ($plain === '') { $maybeJson = json_decode($bodyRaw, true); if (is_array($maybeJson) && isset($maybeJson['status'])) { return ['state' => 1, 'msg' => '成功(明文)', 'body' => $maybeJson]; } return ['state' => -1, 'msg' => '解密失败(请核对网关 SK 是否为 16 位且与 AK 匹配)', 'response' => mb_substr($bodyRaw, 0, 500)]; } $decoded = json_decode($plain, true); return ['state' => 1, 'msg' => '成功', 'body' => is_array($decoded) ? $decoded : []]; } private static function encrypt(string $string, string $key): string { $out = openssl_encrypt($string, 'AES-128-ECB', $key, OPENSSL_RAW_DATA); return $out !== false ? base64_encode($out) : ''; } private static function decrypt(string $string, string $key): string { $bin = base64_decode($string, true); if ($bin === false) { return ''; } $out = openssl_decrypt($bin, 'AES-128-ECB', $key, OPENSSL_RAW_DATA); return $out !== false ? $out : ''; } private static function randStr(int $length = 8): string { $chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; $chars = str_shuffle($chars); $end = strlen($chars) - 1; $buf = []; while (true) { $c = $chars[random_int(0, $end)]; if ($c !== '0') { $buf[] = $c; break; } } $n = 1; while ($n < $length) { $r = $chars[random_int(0, $end)]; if ($r !== $buf[count($buf) - 1]) { $buf[] = $r; ++$n; } } return implode('', $buf); } }