347 lines
16 KiB
PHP
347 lines
16 KiB
PHP
<?php
|
||
|
||
namespace addon\cypay\sdk;
|
||
require_once 'AopEncrypt.php';
|
||
|
||
class AopClient
|
||
{
|
||
//网关
|
||
public $gatewayUrl = "https://pay.chanpay.com/ugtw/gateway";
|
||
//返回数据格式
|
||
public $format = "json";
|
||
//api版本
|
||
public $apiVersion = "1.0";
|
||
// 表单提交字符集编码
|
||
public $postCharset = "UTF-8";
|
||
public $fileCharset = "UTF-8";
|
||
|
||
//签名类型
|
||
public $signType = "RSA2";
|
||
public $acq_sp_id = '49072480';//合作机构号
|
||
public $productCode = '1000';//营销产品编码,由畅捷支付分配
|
||
//商户公钥
|
||
public $publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQedhgpTUxuWDof5uiHWszPP/462Q/ORos8uV11g8nYmchcyE3x8jOXE5ZKRAua5HUDNkLQG/8wu4+tPVicKe3r9DOefZf+zxZgHB/uddA+Sl54h9BzjPB8RscBtDM2DWRZhdnVu4LMRb9Lp9eRYTLbviPazCEm13FqttHT35oAQIDAQAB';
|
||
public $rsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpJyDZDMqNmLRKfXclpFu4WbB93ZBmISg1FLrcaIqGRqRttCE/yHxfLTJl7xMxeYXjBqjkiGvnfLbgkocAYWPNER5Urh5aRRORgJIPRr5sTpVMHKRrok7yfaA/t7D/7AHhrPVxoE/nv5aMT4oGtTJhIxyLdnuK5pbHoQR9lODx+kOLgZ1xlJmVMD+ewZVrF4sllIGoOJRkjkUOb+xRD11PF2Lk4w3u+AUiLukV1vy98rVsBmL09DCNsG4d4DZkSPtGbGDFwoMh07+SR02cIb2WiRtQnPaYKSYdkMHDqOH3XUawFgHHoYLugL/57c1y5HG6yx2zZUVQ4yPspfX2zSZrAgMBAAECggEACoSHU1erRJCpLTSN8wY8OcNB6SGct+z53gsS71+EtYKw+K1Jn/isWxKpXpM2A06GF66zU7pz0yn7CQ2zXT+w//A/jY5iDsTayaJP8qk4b+2W9OuXAaZO+F79VtjqJY+cMlcZz93i+gr8pm7Pq0ka/9U6EiXk2qcp2vHVIKXgMst9xJLzFamhXiWbBaIENigzXV/uqCC4w48KVCUDOtpQTkaoSLvZ0uChuYPLDSEsuKp0wM2BLlJ0cZ/bdhfII2qWyvIcdTM0If+d/phigg4OUX/tZqfLH/k0NGK3k1TQhMbqHu0TwbjdJ7c023wwmzxIRsl0yl5FtJBA1x1/phZWUQKBgQDvKZqXBUiULZM0et9Y5AtaTtrgbB3c6ZIV4dcF8beeHFhL4QsSq33k44YdAS3qWaa+eOWfrDOd+G2Pf6F3sNhgLGphlrVIByoP0r0FCfpmSpozhO+pLJBgXhnrZ+psJ0yaMWrz5J5POc8iPNWlVIllucv9tnUnTYkKIU2Cllf/eQKBgQC1D7+4S/YkM5kzZOb1NqwaYAkZCGDFKHL856j1dUvdEIK+2d67M74v8Op5OSVX4g4vKMMIhmdRLia4TL/kgePvH7sLml+BxCwrHhBGyMYBkoeh1ohwjW/tSe3TJsKtwASuY7R/Wyb0mJbxkAxHECozs/5VtkJYwxZqf19QKNxoAwKBgD4kvFODfvFpyjc3ujM5xi1oEf2Ael39nwTqktmrjj+aM+M7jYoDX4oLCL0eolSjiO0zMs9DioIAnE9OJaGZJRAQWnATHfWiTu6fnpfhmNvdhKXgY/m8Z6NysB939/S0XXYvYxAOlogViFnoHsd/6Ney6Gt7boOQ5QvpzV8iO6lZAoGAUIJJOJSmRSCgbYbfX4fI7Q1o3jWoeeJrhuMncMWQTyLpUB2meU0fs0eHqxFq9nHw5q1UU7UXubQwyWBvLxdGl+xfCmDBOP1WunFqwV7DFK3oG2E+V/W8ICHwWyRwCjxImJaDCuIoJzi2XYE0xGB+s3DEla4uQDO3AvHSGt2ga6sCgYEA3rm1Q/NFBI+QPLsFIKOmsWhdKrJOwe74rqeovXfI8dfOs1s2G3a+Ygu7vT67BB+8mqiPxRRcs+P+2eTQ9IMhn7gjQRq/iGY5IJO7Cbm9hpiSoBkRpSOEFev6210+ERtUDva/W+C6E6INTjgtwaOVHqnfK0qB8ZWTv1iteXNPOV0=';
|
||
// 畅捷公钥 RSA2
|
||
public $cjRsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsNU7PbTNvy16IF5sjzMxS3wQo0kvkq3iNmIIbcLnhatlbdeqdTG+P4wnSM+28Eums/izjL2TKdshoPcuvZ18/rGOUprCeeL7VfTtKu/mlS5H+FCXkdG0a0kstvjloqwRcksV2IiAiFttdB85zI4stu4NBMh4oFd5+nO8Jt7JMPI1J/4zd6VGn4bJ6DEcKVfXn+xpZ9fp0LRRREB5AbqJKpSLS7G+cQE1YT9kRHnAvCZcE+WPwwv24lIOiEXcNacz1vuDC4kZf3ahweuHAGlWve00/O9iIzSkB0FazUGLjOf6WcQiugEz1DtL8wKW2OlAwuk8VU3sSuLbDmzLdYG84QIDAQAB';
|
||
|
||
public $is_isp = false;
|
||
|
||
|
||
public function generateSign($params, $signType = "RSA2")
|
||
{
|
||
$params = array_filter($params);
|
||
$params['sign_type'] = $signType;
|
||
return $this->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';
|
||
}
|
||
}
|