sign($this->getSignContent($params), $signType); } public function getSignContent($params) { // 请求参数转字符串 $signStr = 'method' . '=' . $params['method'] . '&'; if ($this->is_isp) { $signStr .= 'acq_sp_id' . '=' . $params['acq_sp_id'] . '&'; } $signStr .= 'function_code' . '=' . $params['function_code'] . '&'; $signStr .= 'product_code' . '=' . $params['product_code'] . '&'; $signStr .= 'biz_content' . '=' . $params['biz_content']; $signStr = trim($signStr, '&'); return $signStr; } protected function sign($data, $signType = "RSA") { if (!$this->checkEmpty($this->rsaPrivateKey)) { $priKey = $this->rsaPrivateKey; $res = "-----BEGIN RSA PRIVATE KEY-----\n" . wordwrap($priKey, 64, "\n", true) . "\n-----END RSA PRIVATE KEY-----"; } else { if (!$this->rsaPrivateKey || empty($priKey)) throw new \Exception("您使用的私钥格式错误,请检查RSA私钥配置"); } if ("RSA2" == $signType) { openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256); } else { openssl_sign($data, $sign, $res); } $sign = base64_encode($sign); return $sign; } /** * RSA单独签名方法,未做字符串处理,字符串处理见getSignContent() * @param $data 待签名字符串 * @param $privatekey 商户私钥,根据keyfromfile来判断是读取字符串还是读取文件,false:填写私钥字符串去回车和空格 true:填写私钥文件路径 * @param $signType 签名方式,RSA:SHA1 RSA2:SHA256 * @param $keyfromfile 私钥获取方式,读取字符串还是读文件 * @return string */ protected function curl($url, $post_data = array(), $timeout = 5, $header = "", $data_type = "") { $header = empty($header) ? '' : $header; //支持json数据数据提交 if ($data_type == 'json') { $post_string = json_encode($post_data); } elseif ($data_type == 'array') { $post_string = $post_data; } elseif (is_array($post_data)) { $post_string = http_build_query($post_data, '', '&'); } $ch = curl_init(); // 启动一个CURL会话 curl_setopt($ch, CURLOPT_URL, $url); // 要访问的地址 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 对认证证书来源的检查 // https请求 不验证证书和hosts curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在 curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT'] ?? ''); // 模拟用户使用的浏览器 //curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 使用自动跳转 //curl_setopt($curl, CURLOPT_AUTOREFERER, 1); // 自动设置Referer curl_setopt($ch, CURLOPT_POST, true); // 发送一个常规的Post请求 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string); // Post提交的数据包 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); // 设置超时限制防止死循环 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); //curl_setopt($curl, CURLOPT_HEADER, 0); // 显示返回的Header区域内容 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 获取的信息以文件流的形式返回 if ($header) curl_setopt($ch, CURLOPT_HTTPHEADER, $header); //模拟的header头 $result = curl_exec($ch); curl_close($ch); return $result; } public function execute($request) { $this->setupCharsets($request); //如果两者编码不一致,会出现签名验签或者乱码 if (strcasecmp($this->fileCharset, $this->postCharset)) { // writeLog("本地文件字符集编码与表单提交编码不一致,请务必设置成一样,属性名分别为postCharset!"); throw new \Exception("文件编码:[" . $this->fileCharset . "] 与表单提交编码:[" . $this->postCharset . "]两者不一致!"); } $iv = null; if (!$this->checkEmpty($request->getApiVersion())) { $iv = $request->getApiVersion(); } else { $iv = $this->apiVersion; } //组装系统参数 $sysParams["method"] = $request->getApiMethodName(); if ($this->is_isp) { $sysParams["acq_sp_id"] = $this->acq_sp_id; if (!$this->checkEmpty($request->getSpId())) { $sysParams["acq_sp_id"] = $request->getSpId(); } } $sysParams["product_code"] = $this->productCode; if (!$this->checkEmpty($request->getProdCode())) { $sysParams["product_code"] = $request->getProdCode(); } $sysParams["function_code"] = $request->getFunctionCode(); $sysParams["format"] = $this->format; $sysParams["charset"] = $this->postCharset; $sysParams["sign_type"] = $this->signType; $sysParams["timestamp"] = date("YmdHis"); $sysParams["version"] = $iv; $apiParams = $request->getApiParas(); // 执行加密 if (empty($apiParams['biz_content'])) { throw new \Exception(" api request Fail! The reason : encrypt request is not supperted!"); } if (!$this->checkEmpty($request->getNotifyUrl())) { if (is_array($apiParams['biz_content'])) { $apiParams['biz_content']["notify_url"] = $request->getNotifyUrl(); } else { $apiParams['biz_content'] = json_decode($apiParams['biz_content'], true); $apiParams['biz_content']["notify_url"] = $request->getNotifyUrl(); } } if (is_array($apiParams['biz_content'])) { $apiParams['biz_content'] = json_encode($apiParams['biz_content']); } $enCryptContent = $this->rsaEncrypt($apiParams['biz_content']); if (empty($enCryptContent)) { throw new \Exception("加密失败!"); } $sysParams['biz_content'] = $enCryptContent; //签名 $sysParams["sign"] = $this->generateSign($sysParams, $this->signType); //发起HTTP请求 try { $resp = $this->curl($this->gatewayUrl, $sysParams); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } //解析AOP返回结果 $respObject = ''; // 将返回结果转换本地文件编码 $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp); if ("json" == strtolower($this->format)) { $respObject = json_decode($r, true); } // 验签 $CheckSign = $this->rsaCheckResp($respObject); if ($CheckSign) { $content = $respObject['content'] ?? ''; if (!empty($content)) { $respObject['content'] = $this->rsaDecrypt($content); } } else { throw new \Exception($respObject['sub_msg']); } return $respObject; } /** * 校验$value是否非空 * if not set ,return true; * if is null , return true; **/ protected function checkEmpty($value) { if (!isset($value)) return true; if ($value === null) return true; if (trim($value) === "") return true; return false; } /*** * 获取签名 * @param $params * @return string */ public function getVerSignContent($params) { $signStr = ''; $signStr .= 'merchant_id' . '=' . $params['merchant_id'] . '&'; $signStr .= 'product_code' . '=' . $params['product_code'] . '&'; $signStr .= 'function_code' . '=' . $params['function_code'] . '&'; $signStr .= 'biz_content' . '=' . $params['biz_content']; $signStr = trim($signStr, '&'); return $signStr; } /** rsaCheckV1 & rsaCheckV2 * 验证签名 * 在使用本方法前,必须初始化AopClient且传入公钥参数。 * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 **/ public function rsaCheckV1($params, $rsaPublicKey = '', $signType = 'RSA2') { $sign = $params['sign']; unset($params['sign']); unset($params['sign_type']); return $this->verify($this->getVerSignContent($params), $sign, $rsaPublicKey, $signType); } /*** * 请求返回结果验签 * @param $params * @param $rsaPublicKey * @param $signType * @return bool * @throws \Exception */ public function rsaCheckResp($params, $rsaPublicKey = '', $signType = 'RSA2') { $sign = $params['sign']; $signStr = ''; $signStr .= 'code' . '=' . $params['code'] . '&'; $signStr .= 'msg' . '=' . $params['msg'] . '&'; $signStr .= 'sub_code' . '=' . $params['sub_code'] . '&'; $signStr .= 'sub_msg' . '=' . $params['sub_msg'] . '&'; $signStr .= 'content' . '=' . $params['content']; $signStr = trim($signStr, '&'); return $this->verify($signStr, $sign, $rsaPublicKey, $signType); } function verify($data, $sign, $pubKeyFile = '', $signType = 'RSA') { if ($this->checkEmpty($pubKeyFile)) { $pubKey = $this->cjRsaPublicKey; $res = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($pubKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; } else { $pubKey = file_get_contents($pubKeyFile); $res = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($pubKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; } if (!$this->cjRsaPublicKey || empty($pubKey)) throw new \Exception("支付宝RSA公钥错误。请检查公钥文件格式是否正确"); //调用openssl内置方法验签,返回bool值 $result = FALSE; if ("RSA2" == $signType) { $result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256) === 1); } else { $result = (openssl_verify($data, base64_decode($sign), $res) === 1); } return $result; } /** * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 **/ public function rsaEncrypt($bizContentJson) { if (!$this->checkEmpty($this->cjRsaPublicKey)) { //读字符串 $priKey = $this->cjRsaPublicKey; $publicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($priKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; } else { throw new \Exception("支付宝RSA私钥错误。请检查公钥文件格式是否正确"); } // 加密每段最大245,解密每段最大256 $bizContentArray = mb_str_split($bizContentJson, 245, 'utf-8'); $useBizContent = ''; foreach ($bizContentArray as $bizContentString) { openssl_public_encrypt($bizContentString, $encryptedChunk, $publicKey); $useBizContent .= $encryptedChunk; } return base64_encode($useBizContent); } /** * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 **/ public function rsaDecrypt($content) { if (!$this->checkEmpty($this->rsaPrivateKey)) { //读字符串 $priKey = $this->rsaPrivateKey; $res = "-----BEGIN RSA PRIVATE KEY-----\n" . wordwrap($priKey, 64, "\n", true) . "\n-----END RSA PRIVATE KEY-----"; } else { throw new \Exception("RSA私钥错误。请检查公钥文件格式是否正确"); } $privateKey = openssl_get_privatekey($res); // 信息解密 $bizContentArray = str_split(base64_decode($content), 256); $content = ''; foreach ($bizContentArray as $bizContentString) { openssl_private_decrypt($bizContentString, $decrypted, $privateKey, OPENSSL_PKCS1_PADDING); $content .= $decrypted; } return is_json($content) ? json_decode($content, true) : []; } private function setupCharsets($request) { if ($this->checkEmpty($this->postCharset)) { $this->postCharset = 'UTF-8'; } $str = print_r($request, true); $this->fileCharset = mb_detect_encoding($str, "UTF-8, GBK") == 'UTF-8' ? 'UTF-8' : 'GBK'; } }