mirror of https://gitee.com/karson/fastadmin.git
448 lines
17 KiB
PHP
448 lines
17 KiB
PHP
<?php
|
||
|
||
namespace fast\payment;
|
||
|
||
use DOMDocument;
|
||
use Exception;
|
||
|
||
/**
|
||
* 支付宝
|
||
* @link https://github.com/mytharcher/alipay-php-sdk
|
||
*/
|
||
class Alipay
|
||
{
|
||
|
||
const SERVICE = 'create_direct_pay_by_user';
|
||
const SERVICE_WAP = 'alipay.wap.trade.create.direct';
|
||
const SERVICE_WAP_AUTH = 'alipay.wap.auth.authAndExecute';
|
||
const SERVICE_APP = 'mobile.securitypay.pay';
|
||
const GATEWAY = 'https://mapi.alipay.com/gateway.do?';
|
||
const GATEWAY_MOBILE = 'http://wappaygw.alipay.com/service/rest.htm?';
|
||
const VERIFY_URL = 'http://notify.alipay.com/trade/notify_query.do?';
|
||
const VERIFY_URL_HTTPS = 'https://mapi.alipay.com/gateway.do?service=notify_verify&';
|
||
|
||
// 配置信息在实例化时传入,以下为范例
|
||
private $config = array(
|
||
// 即时到账方式
|
||
'payment_type' => 1,
|
||
// 传输协议
|
||
'transport' => 'http',
|
||
// 编码方式
|
||
'input_charset' => 'utf-8',
|
||
// 签名方法
|
||
'sign_type' => 'MD5',
|
||
// 证书路径
|
||
'cacert' => './cacert.pem',
|
||
//验签公钥地址
|
||
'public_key_path' => './alipay_public_key.pem',
|
||
'private_key_path' => '',
|
||
// 支付完成异步通知调用地址
|
||
// 'notify_url' => 'http://'.$_SERVER['HTTP_HOST'].'/order/callback_alipay/notify',
|
||
// 支付完成同步返回地址
|
||
// 'return_url' => 'http://'.$_SERVER['HTTP_HOST'].'/order/callback_alipay/return',
|
||
// 支付宝商家 ID
|
||
'partner' => '2088xxxxxxxx',
|
||
// // 支付宝商家 KEY
|
||
'key' => 'xxxxxxxxxxxx',
|
||
// // 支付宝商家注册邮箱
|
||
'seller_email' => 'email@domain.com'
|
||
);
|
||
private $is_mobile = FALSE;
|
||
public $service = self::SERVICE;
|
||
public $gateway = self::GATEWAY;
|
||
|
||
/**
|
||
* 配置
|
||
* @param $options array 配置信息
|
||
* @param null $type string 类型 wap app
|
||
*/
|
||
public function __construct($options = [], $type = null)
|
||
{
|
||
if ($config = Config::get('payment.alipay'))
|
||
{
|
||
$this->config = array_merge($this->config, $config);
|
||
}
|
||
$this->config = array_merge($this->config, is_array($options) ? $options : []);
|
||
$this->is_mobile = (($type == 'wap' || $type === true) ? true : false);
|
||
if ($this->is_mobile)
|
||
{
|
||
$this->gateway = self::GATEWAY_MOBILE;
|
||
}
|
||
|
||
if ($type == 'wap' || $type === true)
|
||
{
|
||
$this->service = self::SERVICE_WAP;
|
||
}
|
||
elseif ($type == 'app')
|
||
{
|
||
$this->service = self::SERVICE_APP;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成请求参数的签名
|
||
*
|
||
* @param $params <Array>
|
||
* @return <String>
|
||
*
|
||
*/
|
||
function signParameters($params)
|
||
{
|
||
// 支付宝的签名串必须是未经过 urlencode 的字符串
|
||
// 不清楚为何 PHP 5.5 里没有 http_build_str() 方法
|
||
$paramStr = urldecode(http_build_query($params));
|
||
switch (strtoupper(trim($this->config['sign_type'])))
|
||
{
|
||
case "MD5" :
|
||
$result = md5($paramStr . $this->config['key']);
|
||
break;
|
||
case "RSA" :
|
||
case "0001" :
|
||
$priKey = file_get_contents($this->config['private_key_path']);
|
||
$res = openssl_get_privatekey($priKey);
|
||
openssl_sign($paramStr, $sign, $res);
|
||
openssl_free_key($res);
|
||
//base64编码
|
||
$result = base64_encode($sign);
|
||
break;
|
||
default :
|
||
$result = "";
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 准备签名参数
|
||
*
|
||
* @param $params <Array>
|
||
* $params['out_trade_no'] 唯一订单编号
|
||
* $params['subject']
|
||
* $params['total_fee']
|
||
* $params['body']
|
||
* $params['show_url']
|
||
* $params['anti_phishing_key']
|
||
* $params['exter_invoke_ip']
|
||
* $params['it_b_pay']
|
||
* $params['_input_charset']
|
||
* @return <Array>
|
||
*/
|
||
function prepareParameters($params)
|
||
{
|
||
$default = array(
|
||
'service' => $this->service,
|
||
'partner' => $this->config['partner'],
|
||
'_input_charset' => trim(strtolower($this->config['input_charset']))
|
||
);
|
||
if (!$this->is_mobile)
|
||
{
|
||
$default = array_merge($default, array(
|
||
'payment_type' => $this->config['payment_type'],
|
||
'seller_id' => $this->config['partner'],
|
||
'notify_url' => $this->config['notify_url'],
|
||
));
|
||
if (isset($this->config['return_url']))
|
||
{
|
||
$default['return_url'] = $this->config['return_url'];
|
||
}
|
||
}
|
||
|
||
$params = $this->filterSignParameter(array_merge($default, (array) $params));
|
||
ksort($params);
|
||
reset($params);
|
||
return $params;
|
||
}
|
||
|
||
/**
|
||
* 生成签名后的请求参数
|
||
*
|
||
*/
|
||
function buildSignedParameters($params)
|
||
{
|
||
$params = $this->prepareParameters($params);
|
||
$params['sign'] = $this->signParameters($params);
|
||
if ($params['service'] != self::SERVICE_WAP && $params['service'] != self::SERVICE_WAP_AUTH)
|
||
{
|
||
$params['sign_type'] = strtoupper(trim($this->config['sign_type']));
|
||
}
|
||
return $params;
|
||
}
|
||
|
||
/**
|
||
* https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0.NgdeQA&treeId=59&articleId=103663&docType=1
|
||
* 服务端生成app支付使用的参数以及签名
|
||
* @param $params <Array>
|
||
* @return <Array>
|
||
*/
|
||
function buildSignedParametersForApp($params)
|
||
{
|
||
$params = $this->prepareParameters($params);
|
||
$params['sign'] = urlencode($this->signParameters($params));
|
||
$params['sign_type'] = 'RSA';
|
||
$paramStr = [];
|
||
foreach ($params as $k => &$param)
|
||
{
|
||
$param = '"' . $param . '"';
|
||
$paramStr[] = $k . '=' . $param;
|
||
}
|
||
|
||
return implode('&', $paramStr);
|
||
}
|
||
|
||
/**
|
||
* 生成请求参数的发送表单HTML
|
||
*
|
||
* 其实这个函数没有必要,更应该使用签名后的参数自己组装,只不过有时候方便就从官方 SDK 里留下了。
|
||
*
|
||
* @param $params <Array> 请求参数(未签名的)
|
||
* @param $method <String> 请求方法,默认:post,可选 get
|
||
* @param $target <String> 提交目标,默认:_self
|
||
* @return <String>
|
||
*
|
||
*/
|
||
function buildRequestFormHTML($params, $method = 'post', $target = '_self')
|
||
{
|
||
$params = $this->buildSignedParameters($params);
|
||
$html = '<meta charset="' . $this->config['input_charset'] . '" /><form id="alipaysubmit" name="alipaysubmit" action="' . $this->gateway . ' _input_charset="' . trim(strtolower($this->config['input_charset'])) . '" method="' . $method . ' target="$target">';
|
||
foreach ($params as $key => $value)
|
||
{
|
||
$html .= "<input type='hidden' name='$key' value='$value'/>";
|
||
}
|
||
$html .= "</form><script>document.forms['alipaysubmit'].submit();</script>";
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* 准备移动网页支付的请求参数
|
||
*
|
||
* 移动网页支付接口不同,需要先服务器提交一次请求,拿到返回 token 再返回客户端发起真实支付请求。
|
||
* 该方法只完成第一次服务端请求,生成参数后需要客户端另行处理(可调用`buildRequestFormHTML`生成表单提交)。
|
||
*
|
||
* @param $params <Array>
|
||
* $params['out_trade_no'] 订单唯一编号
|
||
* $params['subject'] 商品标题
|
||
* $params['total_fee'] 支付总费用
|
||
* $params['merchant_url'] 商品链接地址
|
||
* $params['req_id'] 请求唯一 ID
|
||
* $params['it_b_pay'] 超期时间(秒)
|
||
* @return <Array>/<NULL>
|
||
*/
|
||
function prepareMobileTradeData($params)
|
||
{
|
||
// 不要用 SimpleXML 来构建 xml 结构,因为有第一行文档申明支付宝验证不通过
|
||
$xml_str = '<direct_trade_create_req>' .
|
||
'<notify_url>' . $this->config['notify_url'] . '</notify_url>' .
|
||
'<call_back_url>' . $this->config['return_url'] . '</call_back_url>' .
|
||
'<seller_account_name>' . $this->config['seller_email'] . '</seller_account_name>' .
|
||
'<out_trade_no>' . $params['out_trade_no'] . '</out_trade_no>' .
|
||
'<subject>' . htmlspecialchars($params['subject'], ENT_XML1, 'UTF-8') . '</subject>' .
|
||
'<total_fee>' . $params['total_fee'] . '</total_fee>' .
|
||
'<merchant_url>' . $params['merchant_url'] . '</merchant_url>' .
|
||
(isset($params['it_b_pay']) ? '<pay_expire>' . $params['it_b_pay'] . '</pay_expire>' : '') .
|
||
'</direct_trade_create_req>';
|
||
$request_data = $this->buildSignedParameters(array(
|
||
'service' => $this->service,
|
||
'partner' => $this->config['partner'],
|
||
'sec_id' => $this->config['sign_type'],
|
||
'format' => 'xml',
|
||
'v' => '2.0',
|
||
'req_id' => $params['req_id'],
|
||
'req_data' => $xml_str
|
||
));
|
||
$url = $this->gateway;
|
||
$input_charset = trim(strtolower($this->config['input_charset']));
|
||
if (trim($input_charset) != '')
|
||
{
|
||
$url = $url . "_input_charset=" . $input_charset;
|
||
}
|
||
$curl = curl_init($url);
|
||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //SSL证书认证
|
||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); //严格认证
|
||
curl_setopt($curl, CURLOPT_CAINFO, $this->config['cacert']); //证书地址
|
||
curl_setopt($curl, CURLOPT_HEADER, 0); // 过滤HTTP头
|
||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 显示输出结果
|
||
curl_setopt($curl, CURLOPT_POST, true); // post传输数据
|
||
curl_setopt($curl, CURLOPT_POSTFIELDS, $request_data); // post传输数据
|
||
$responseText = curl_exec($curl);
|
||
//var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
|
||
curl_close($curl);
|
||
if (empty($responseText))
|
||
{
|
||
return NULL;
|
||
}
|
||
parse_str($responseText, $responseData);
|
||
if (empty($responseData['res_data']))
|
||
{
|
||
return NULL;
|
||
}
|
||
if ($this->config['sign_type'] == '0001')
|
||
{
|
||
$responseData['res_data'] = $this->rsaDecrypt($responseData['res_data'], $this->config['private_key_path']);
|
||
}
|
||
//token从res_data中解析出来(也就是说res_data中已经包含token的内容)
|
||
$doc = new DOMDocument();
|
||
$doc->loadXML($responseData['res_data']);
|
||
$responseData['request_token'] = $doc->getElementsByTagName("request_token")->item(0)->nodeValue;
|
||
$xml_str = '<auth_and_execute_req>' .
|
||
'<request_token>' . $responseData['request_token'] . '</request_token>' .
|
||
'</auth_and_execute_req>';
|
||
return array(
|
||
'service' => self::SERVICE_WAP_AUTH,
|
||
'partner' => $this->config['partner'],
|
||
'sec_id' => $this->config['sign_type'],
|
||
'format' => 'xml',
|
||
'v' => '2.0',
|
||
'req_data' => $xml_str
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 支付完成验证返回参数(包含同步和异步)
|
||
*
|
||
* @return <Boolean>
|
||
*/
|
||
function verifyCallback()
|
||
{
|
||
$async = empty($_GET);
|
||
$data = $async ? $_POST : $_GET;
|
||
if (empty($data))
|
||
{
|
||
return FALSE;
|
||
}
|
||
$signValid = $this->verifyParameters($data, $data["sign"]);
|
||
$notify_id = isset($data['notify_id']) ? $data['notify_id'] : NULL;
|
||
if ($async && $this->is_mobile)
|
||
{
|
||
//对notify_data解密
|
||
if ($this->config['sign_type'] == '0001')
|
||
{
|
||
$data['notify_data'] = $this->rsaDecrypt($data['notify_data'], $this->config['private_key_path']);
|
||
}
|
||
//notify_id从decrypt_post_para中解析出来(也就是说decrypt_post_para中已经包含notify_id的内容)
|
||
$doc = new DOMDocument();
|
||
$doc->loadXML($data['notify_data']);
|
||
$notify_id = $doc->getElementsByTagName('notify_id')->item(0)->nodeValue;
|
||
}
|
||
//获取支付宝远程服务器ATN结果(验证是否是支付宝发来的消息)
|
||
$responseTxt = 'true';
|
||
if (!empty($notify_id))
|
||
{
|
||
$responseTxt = $this->verifyFromServer($notify_id);
|
||
}
|
||
//验证
|
||
//$signValid的结果不是true,与安全校验码、请求时的参数格式(如:带自定义参数等)、编码格式有关
|
||
//$responsetTxt的结果不是true,与服务器设置问题、合作身份者ID、notify_id一分钟失效有关
|
||
return $signValid && preg_match("/true$/i", $responseTxt);
|
||
}
|
||
|
||
function verifyParameters($params, $sign)
|
||
{
|
||
$params = $this->filterSignParameter($params);
|
||
if (isset($params['notify_data']))
|
||
{
|
||
$params = array(
|
||
'service' => $params['service'],
|
||
'v' => $params['v'],
|
||
'sec_id' => $params['sec_id'],
|
||
'notify_data' => $params['notify_data']
|
||
);
|
||
}
|
||
else
|
||
{
|
||
ksort($params);
|
||
reset($params);
|
||
}
|
||
$content = urldecode(http_build_query($params));
|
||
switch (strtoupper(trim($this->config['sign_type'])))
|
||
{
|
||
case "MD5" :
|
||
return md5($content . $this->config['key']) == $sign;
|
||
case "RSA" :
|
||
case "0001" :
|
||
return $this->rsaVerify($content, $this->config['public_key_path'], $sign);
|
||
default :
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 过滤参数,去除sign/sign_type参数
|
||
* @param $params
|
||
* @return <Array>
|
||
*/
|
||
function filterSignParameter($params)
|
||
{
|
||
$result = array();
|
||
foreach ($params as $key => $value)
|
||
{
|
||
if ($key != 'sign' && $key != 'sign_type' && $value)
|
||
{
|
||
$result[$key] = $value;
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
function verifyFromServer($notify_id)
|
||
{
|
||
$transport = strtolower(trim($this->config['transport']));
|
||
$partner = trim($this->config['partner']);
|
||
$veryfy_url = ($transport == 'https' ? self::VERIFY_URL_HTTPS : self::VERIFY_URL) . "partner=$partner¬ify_id=$notify_id";
|
||
$curl = curl_init($veryfy_url);
|
||
curl_setopt($curl, CURLOPT_HEADER, 0); // 过滤HTTP头
|
||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //SSL证书认证
|
||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); //严格认证
|
||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 显示输出结果
|
||
curl_setopt($curl, CURLOPT_CAINFO, $this->config['cacert']); //证书地址
|
||
$responseText = curl_exec($curl);
|
||
// var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
|
||
curl_close($curl);
|
||
return $responseText;
|
||
}
|
||
|
||
/**
|
||
* RSA验签,注意验签的公钥是支付宝的公钥,不是自己生成的rsa公钥,可以在淘宝的demo中获得
|
||
* @param $data string 待签名数据
|
||
* @param $ali_public_key_path string 支付宝的公钥文件路径
|
||
* @param $sign string 要校对的的签名结果
|
||
* @return <Boolean> 验证结果
|
||
* @throws Exception
|
||
*/
|
||
function rsaVerify($data, $ali_public_key_path, $sign)
|
||
{
|
||
$pubKey = file_get_contents($ali_public_key_path);
|
||
$res = openssl_get_publickey($pubKey);
|
||
if (!$res)
|
||
{
|
||
throw new Exception('公钥格式错误');
|
||
}
|
||
$result = (bool) openssl_verify($data, base64_decode($sign), $res);
|
||
openssl_free_key($res);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* RSA解密
|
||
* @param $content string 需要解密的内容,密文
|
||
* @param $private_key_path string 商户私钥文件路径
|
||
* @return string 解密后内容,明文
|
||
*/
|
||
function rsaDecrypt($content, $private_key_path)
|
||
{
|
||
$priKey = file_get_contents($private_key_path);
|
||
$res = openssl_get_privatekey($priKey);
|
||
//用base64将内容还原成二进制
|
||
$content = base64_decode($content);
|
||
//把需要解密的内容,按128位拆开解密
|
||
$result = '';
|
||
for ($i = 0; $i < strlen($content) / 128; $i++)
|
||
{
|
||
$data = substr($content, $i * 128, 128);
|
||
openssl_private_decrypt($data, $decrypt, $res);
|
||
$result .= $decrypt;
|
||
}
|
||
openssl_free_key($res);
|
||
return $result;
|
||
}
|
||
|
||
}
|