conf = $conf ?: (array) config('qyt'); $this->privateKey = $this->loadPrivateKey($this->conf['private_key'] ?? ''); $this->platformPublicKey = $this->loadPublicKey($this->conf['platform_public_key'] ?? ''); } // ================================================================ // 1. 业务信封 // ================================================================ /** * 构建业务请求信封 * @param array $bizParams 业务参数(会被 json_encode 成字符串放进 params) * @return array{appId:string,timestamp:int,sign:string,params:string} */ public function envelope(array $bizParams): array { $appId = (string) ($this->conf['app_id'] ?? ''); $timestamp = time(); // ⚠️ params 必须是 JSON 字符串(转义),不是对象 $paramsJson = json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $sign = $this->bizSign([ 'appId' => $appId, 'timestamp' => $timestamp, 'params' => $paramsJson, ]); return [ 'appId' => $appId, 'timestamp' => $timestamp, 'sign' => $sign, 'params' => $paramsJson, ]; } /** * 业务信封签名 * 【TODO 待对接人确认】业务接口的具体拼签字段与顺序,文档 §2.4 只给了 7.15 的算法, * 业务接口写的是「密钥、公私钥与具体拼签字段请向对接人员索取」。 * 当前默认实现:对参与字段 ksort 后 http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64 * (与 7.15 同族算法,落地时按实际规则替换本方法即可,调用方无需改动) * * @param array $fields 参与签名的字段(appId / timestamp / params) */ public function bizSign(array $fields): string { $mode = $this->conf['sign_mode'] ?? 'ksort_md5_sha1rsa'; switch ($mode) { // 预留:'raw_concat_sha256rsa' 等其它模式 case 'ksort_md5_sha1rsa': default: return $this->md5Sha1RsaSign($fields); } } // ================================================================ // 2. 7.15 H5 免登地址 // ================================================================ /** * 生成司机免登加油 H5 跳转地址(7.15) * @param string $mobile 司机手机号 * @param string $creditcode 企业营业执照号;传空则不带(不参与签名) * @return string 完整可跳转 URL */ public function h5Url(string $mobile, string $creditcode = ''): string { $base = rtrim((string) ($this->conf['h5_base_url'] ?? ''), '/'); $appId = (string) ($this->conf['h5_app_id'] ?: $this->conf['app_id']); $timestamp = date('YmdHis'); $params = [ 'appId' => $appId, 'mobile' => $mobile, 'timestamp' => $timestamp, ]; if ($creditcode !== '') { $params['creditcode'] = $creditcode; // 传了必须一并入签 } $params['sign'] = $this->md5Sha1RsaSign($params); // 文档示例形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..×tamp=..&sign=..#/ return $base . '/?' . http_build_query($params) . '#/'; } // ================================================================ // 3. 验签(企油通响应 / 回调) // ================================================================ /** * 验签企油通返回 / 回调报文 * @param array $data 业务数据(通用响应里的 data,或回调 params) * @param string $sign 报文里的 sign * 【TODO 待对接人确认】验签串的构造(对 data 的 json 形态 / 是否含外层字段) */ public function verify($data, string $sign): bool { if ($sign === '' || $this->platformPublicKey === false) { return false; } $payload = is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $ok = openssl_verify($payload, base64_decode($sign), $this->platformPublicKey, OPENSSL_ALGO_SHA256); return $ok === 1; } // ================================================================ // 内部:算法与密钥 // ================================================================ /** * 同族签名算法:ksort → http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64 */ protected function md5Sha1RsaSign(array $params): string { ksort($params); $raw = urldecode(http_build_query($params)); $md5Upper = strtoupper(md5($raw)); $signature = ''; openssl_sign($md5Upper, $signature, $this->privateKey, OPENSSL_ALGO_SHA1); return base64_encode($signature); } /** * 载入私钥:支持 PEM 全文 / 去头尾的单行 base64 * @return \OpenSSLAsymmetricKey|resource|string */ protected function loadPrivateKey(string $key) { if ($key === '') { return ''; } if (strpos($key, '-----BEGIN') === false) { $key = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(trim($key), 64, "\n") . "-----END PRIVATE KEY-----\n"; } $res = openssl_pkey_get_private($key); return $res === false ? '' : $res; } /** * 载入公钥:支持 PEM 全文 / 去头尾的单行 base64 * @return \OpenSSLAsymmetricKey|resource|false */ protected function loadPublicKey(string $key) { if ($key === '') { return false; } if (strpos($key, '-----BEGIN') === false) { $key = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(trim($key), 64, "\n") . "-----END PUBLIC KEY-----\n"; } return openssl_pkey_get_public($key); } }