Alipay.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <?php
  2. namespace fast\payment;
  3. use DOMDocument;
  4. use Exception;
  5. /**
  6. * 支付宝
  7. * @link https://github.com/mytharcher/alipay-php-sdk
  8. */
  9. class Alipay
  10. {
  11. const SERVICE = 'create_direct_pay_by_user';
  12. const SERVICE_WAP = 'alipay.wap.trade.create.direct';
  13. const SERVICE_WAP_AUTH = 'alipay.wap.auth.authAndExecute';
  14. const SERVICE_APP = 'mobile.securitypay.pay';
  15. const GATEWAY = 'https://mapi.alipay.com/gateway.do?';
  16. const GATEWAY_MOBILE = 'http://wappaygw.alipay.com/service/rest.htm?';
  17. const VERIFY_URL = 'http://notify.alipay.com/trade/notify_query.do?';
  18. const VERIFY_URL_HTTPS = 'https://mapi.alipay.com/gateway.do?service=notify_verify&';
  19. // 配置信息在实例化时传入,以下为范例
  20. private $config = array(
  21. // 即时到账方式
  22. 'payment_type' => 1,
  23. // 传输协议
  24. 'transport' => 'http',
  25. // 编码方式
  26. 'input_charset' => 'utf-8',
  27. // 签名方法
  28. 'sign_type' => 'MD5',
  29. // 证书路径
  30. 'cacert' => './cacert.pem',
  31. //验签公钥地址
  32. 'public_key_path' => './alipay_public_key.pem',
  33. 'private_key_path' => '',
  34. // 支付完成异步通知调用地址
  35. // 'notify_url' => 'http://'.$_SERVER['HTTP_HOST'].'/order/callback_alipay/notify',
  36. // 支付完成同步返回地址
  37. // 'return_url' => 'http://'.$_SERVER['HTTP_HOST'].'/order/callback_alipay/return',
  38. // 支付宝商家 ID
  39. 'partner' => '2088xxxxxxxx',
  40. // // 支付宝商家 KEY
  41. 'key' => 'xxxxxxxxxxxx',
  42. // // 支付宝商家注册邮箱
  43. 'seller_email' => 'email@domain.com'
  44. );
  45. private $is_mobile = FALSE;
  46. public $service = self::SERVICE;
  47. public $gateway = self::GATEWAY;
  48. /**
  49. * 配置
  50. * @param $options array 配置信息
  51. * @param null $type string 类型 wap app
  52. */
  53. public function __construct($options = [], $type = null)
  54. {
  55. if ($config = Config::get('payment.alipay'))
  56. {
  57. $this->config = array_merge($this->config, $config);
  58. }
  59. $this->config = array_merge($this->config, is_array($options) ? $options : []);
  60. $this->is_mobile = (($type == 'wap' || $type === true) ? true : false);
  61. if ($this->is_mobile)
  62. {
  63. $this->gateway = self::GATEWAY_MOBILE;
  64. }
  65. if ($type == 'wap' || $type === true)
  66. {
  67. $this->service = self::SERVICE_WAP;
  68. }
  69. elseif ($type == 'app')
  70. {
  71. $this->service = self::SERVICE_APP;
  72. }
  73. }
  74. /**
  75. * 生成请求参数的签名
  76. *
  77. * @param $params <Array>
  78. * @return <String>
  79. *
  80. */
  81. function signParameters($params)
  82. {
  83. // 支付宝的签名串必须是未经过 urlencode 的字符串
  84. // 不清楚为何 PHP 5.5 里没有 http_build_str() 方法
  85. $paramStr = urldecode(http_build_query($params));
  86. switch (strtoupper(trim($this->config['sign_type'])))
  87. {
  88. case "MD5" :
  89. $result = md5($paramStr . $this->config['key']);
  90. break;
  91. case "RSA" :
  92. case "0001" :
  93. $priKey = file_get_contents($this->config['private_key_path']);
  94. $res = openssl_get_privatekey($priKey);
  95. openssl_sign($paramStr, $sign, $res);
  96. openssl_free_key($res);
  97. //base64编码
  98. $result = base64_encode($sign);
  99. break;
  100. default :
  101. $result = "";
  102. }
  103. return $result;
  104. }
  105. /**
  106. * 准备签名参数
  107. *
  108. * @param $params <Array>
  109. * $params['out_trade_no'] 唯一订单编号
  110. * $params['subject']
  111. * $params['total_fee']
  112. * $params['body']
  113. * $params['show_url']
  114. * $params['anti_phishing_key']
  115. * $params['exter_invoke_ip']
  116. * $params['it_b_pay']
  117. * $params['_input_charset']
  118. * @return <Array>
  119. */
  120. function prepareParameters($params)
  121. {
  122. $default = array(
  123. 'service' => $this->service,
  124. 'partner' => $this->config['partner'],
  125. '_input_charset' => trim(strtolower($this->config['input_charset']))
  126. );
  127. if (!$this->is_mobile)
  128. {
  129. $default = array_merge($default, array(
  130. 'payment_type' => $this->config['payment_type'],
  131. 'seller_id' => $this->config['partner'],
  132. 'notify_url' => $this->config['notify_url'],
  133. ));
  134. if (isset($this->config['return_url']))
  135. {
  136. $default['return_url'] = $this->config['return_url'];
  137. }
  138. }
  139. $params = $this->filterSignParameter(array_merge($default, (array) $params));
  140. ksort($params);
  141. reset($params);
  142. return $params;
  143. }
  144. /**
  145. * 生成签名后的请求参数
  146. *
  147. */
  148. function buildSignedParameters($params)
  149. {
  150. $params = $this->prepareParameters($params);
  151. $params['sign'] = $this->signParameters($params);
  152. if ($params['service'] != self::SERVICE_WAP && $params['service'] != self::SERVICE_WAP_AUTH)
  153. {
  154. $params['sign_type'] = strtoupper(trim($this->config['sign_type']));
  155. }
  156. return $params;
  157. }
  158. /**
  159. * https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0.NgdeQA&treeId=59&articleId=103663&docType=1
  160. * 服务端生成app支付使用的参数以及签名
  161. * @param $params <Array>
  162. * @return <Array>
  163. */
  164. function buildSignedParametersForApp($params)
  165. {
  166. $params = $this->prepareParameters($params);
  167. $params['sign'] = urlencode($this->signParameters($params));
  168. $params['sign_type'] = 'RSA';
  169. $paramStr = [];
  170. foreach ($params as $k => &$param)
  171. {
  172. $param = '"' . $param . '"';
  173. $paramStr[] = $k . '=' . $param;
  174. }
  175. return implode('&', $paramStr);
  176. }
  177. /**
  178. * 生成请求参数的发送表单HTML
  179. *
  180. * 其实这个函数没有必要,更应该使用签名后的参数自己组装,只不过有时候方便就从官方 SDK 里留下了。
  181. *
  182. * @param $params <Array> 请求参数(未签名的)
  183. * @param $method <String> 请求方法,默认:post,可选 get
  184. * @param $target <String> 提交目标,默认:_self
  185. * @return <String>
  186. *
  187. */
  188. function buildRequestFormHTML($params, $method = 'post', $target = '_self')
  189. {
  190. $params = $this->buildSignedParameters($params);
  191. $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">';
  192. foreach ($params as $key => $value)
  193. {
  194. $html .= "<input type='hidden' name='$key' value='$value'/>";
  195. }
  196. $html .= "</form><script>document.forms['alipaysubmit'].submit();</script>";
  197. return $html;
  198. }
  199. /**
  200. * 准备移动网页支付的请求参数
  201. *
  202. * 移动网页支付接口不同,需要先服务器提交一次请求,拿到返回 token 再返回客户端发起真实支付请求。
  203. * 该方法只完成第一次服务端请求,生成参数后需要客户端另行处理(可调用`buildRequestFormHTML`生成表单提交)。
  204. *
  205. * @param $params <Array>
  206. * $params['out_trade_no'] 订单唯一编号
  207. * $params['subject'] 商品标题
  208. * $params['total_fee'] 支付总费用
  209. * $params['merchant_url'] 商品链接地址
  210. * $params['req_id'] 请求唯一 ID
  211. * $params['it_b_pay'] 超期时间(秒)
  212. * @return <Array>/<NULL>
  213. */
  214. function prepareMobileTradeData($params)
  215. {
  216. // 不要用 SimpleXML 来构建 xml 结构,因为有第一行文档申明支付宝验证不通过
  217. $xml_str = '<direct_trade_create_req>' .
  218. '<notify_url>' . $this->config['notify_url'] . '</notify_url>' .
  219. '<call_back_url>' . $this->config['return_url'] . '</call_back_url>' .
  220. '<seller_account_name>' . $this->config['seller_email'] . '</seller_account_name>' .
  221. '<out_trade_no>' . $params['out_trade_no'] . '</out_trade_no>' .
  222. '<subject>' . htmlspecialchars($params['subject'], ENT_XML1, 'UTF-8') . '</subject>' .
  223. '<total_fee>' . $params['total_fee'] . '</total_fee>' .
  224. '<merchant_url>' . $params['merchant_url'] . '</merchant_url>' .
  225. (isset($params['it_b_pay']) ? '<pay_expire>' . $params['it_b_pay'] . '</pay_expire>' : '') .
  226. '</direct_trade_create_req>';
  227. $request_data = $this->buildSignedParameters(array(
  228. 'service' => $this->service,
  229. 'partner' => $this->config['partner'],
  230. 'sec_id' => $this->config['sign_type'],
  231. 'format' => 'xml',
  232. 'v' => '2.0',
  233. 'req_id' => $params['req_id'],
  234. 'req_data' => $xml_str
  235. ));
  236. $url = $this->gateway;
  237. $input_charset = trim(strtolower($this->config['input_charset']));
  238. if (trim($input_charset) != '')
  239. {
  240. $url = $url . "_input_charset=" . $input_charset;
  241. }
  242. $curl = curl_init($url);
  243. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //SSL证书认证
  244. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); //严格认证
  245. curl_setopt($curl, CURLOPT_CAINFO, $this->config['cacert']); //证书地址
  246. curl_setopt($curl, CURLOPT_HEADER, 0); // 过滤HTTP头
  247. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 显示输出结果
  248. curl_setopt($curl, CURLOPT_POST, true); // post传输数据
  249. curl_setopt($curl, CURLOPT_POSTFIELDS, $request_data); // post传输数据
  250. $responseText = curl_exec($curl);
  251. //var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
  252. curl_close($curl);
  253. if (empty($responseText))
  254. {
  255. return NULL;
  256. }
  257. parse_str($responseText, $responseData);
  258. if (empty($responseData['res_data']))
  259. {
  260. return NULL;
  261. }
  262. if ($this->config['sign_type'] == '0001')
  263. {
  264. $responseData['res_data'] = $this->rsaDecrypt($responseData['res_data'], $this->config['private_key_path']);
  265. }
  266. //token从res_data中解析出来(也就是说res_data中已经包含token的内容)
  267. $doc = new DOMDocument();
  268. $doc->loadXML($responseData['res_data']);
  269. $responseData['request_token'] = $doc->getElementsByTagName("request_token")->item(0)->nodeValue;
  270. $xml_str = '<auth_and_execute_req>' .
  271. '<request_token>' . $responseData['request_token'] . '</request_token>' .
  272. '</auth_and_execute_req>';
  273. return array(
  274. 'service' => self::SERVICE_WAP_AUTH,
  275. 'partner' => $this->config['partner'],
  276. 'sec_id' => $this->config['sign_type'],
  277. 'format' => 'xml',
  278. 'v' => '2.0',
  279. 'req_data' => $xml_str
  280. );
  281. }
  282. /**
  283. * 支付完成验证返回参数(包含同步和异步)
  284. *
  285. * @return <Boolean>
  286. */
  287. function verifyCallback()
  288. {
  289. $async = empty($_GET);
  290. $data = $async ? $_POST : $_GET;
  291. if (empty($data))
  292. {
  293. return FALSE;
  294. }
  295. $signValid = $this->verifyParameters($data, $data["sign"]);
  296. $notify_id = isset($data['notify_id']) ? $data['notify_id'] : NULL;
  297. if ($async && $this->is_mobile)
  298. {
  299. //对notify_data解密
  300. if ($this->config['sign_type'] == '0001')
  301. {
  302. $data['notify_data'] = $this->rsaDecrypt($data['notify_data'], $this->config['private_key_path']);
  303. }
  304. //notify_id从decrypt_post_para中解析出来(也就是说decrypt_post_para中已经包含notify_id的内容)
  305. $doc = new DOMDocument();
  306. $doc->loadXML($data['notify_data']);
  307. $notify_id = $doc->getElementsByTagName('notify_id')->item(0)->nodeValue;
  308. }
  309. //获取支付宝远程服务器ATN结果(验证是否是支付宝发来的消息)
  310. $responseTxt = 'true';
  311. if (!empty($notify_id))
  312. {
  313. $responseTxt = $this->verifyFromServer($notify_id);
  314. }
  315. //验证
  316. //$signValid的结果不是true,与安全校验码、请求时的参数格式(如:带自定义参数等)、编码格式有关
  317. //$responsetTxt的结果不是true,与服务器设置问题、合作身份者ID、notify_id一分钟失效有关
  318. return $signValid && preg_match("/true$/i", $responseTxt);
  319. }
  320. function verifyParameters($params, $sign)
  321. {
  322. $params = $this->filterSignParameter($params);
  323. if (isset($params['notify_data']))
  324. {
  325. $params = array(
  326. 'service' => $params['service'],
  327. 'v' => $params['v'],
  328. 'sec_id' => $params['sec_id'],
  329. 'notify_data' => $params['notify_data']
  330. );
  331. }
  332. else
  333. {
  334. ksort($params);
  335. reset($params);
  336. }
  337. $content = urldecode(http_build_query($params));
  338. switch (strtoupper(trim($this->config['sign_type'])))
  339. {
  340. case "MD5" :
  341. return md5($content . $this->config['key']) == $sign;
  342. case "RSA" :
  343. case "0001" :
  344. return $this->rsaVerify($content, $this->config['public_key_path'], $sign);
  345. default :
  346. return FALSE;
  347. }
  348. }
  349. /**
  350. * 过滤参数,去除sign/sign_type参数
  351. * @param $params
  352. * @return <Array>
  353. */
  354. function filterSignParameter($params)
  355. {
  356. $result = array();
  357. foreach ($params as $key => $value)
  358. {
  359. if ($key != 'sign' && $key != 'sign_type' && $value)
  360. {
  361. $result[$key] = $value;
  362. }
  363. }
  364. return $result;
  365. }
  366. function verifyFromServer($notify_id)
  367. {
  368. $transport = strtolower(trim($this->config['transport']));
  369. $partner = trim($this->config['partner']);
  370. $veryfy_url = ($transport == 'https' ? self::VERIFY_URL_HTTPS : self::VERIFY_URL) . "partner=$partner&notify_id=$notify_id";
  371. $curl = curl_init($veryfy_url);
  372. curl_setopt($curl, CURLOPT_HEADER, 0); // 过滤HTTP头
  373. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //SSL证书认证
  374. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); //严格认证
  375. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 显示输出结果
  376. curl_setopt($curl, CURLOPT_CAINFO, $this->config['cacert']); //证书地址
  377. $responseText = curl_exec($curl);
  378. // var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
  379. curl_close($curl);
  380. return $responseText;
  381. }
  382. /**
  383. * RSA验签,注意验签的公钥是支付宝的公钥,不是自己生成的rsa公钥,可以在淘宝的demo中获得
  384. * @param $data string 待签名数据
  385. * @param $ali_public_key_path string 支付宝的公钥文件路径
  386. * @param $sign string 要校对的的签名结果
  387. * @return <Boolean> 验证结果
  388. * @throws Exception
  389. */
  390. function rsaVerify($data, $ali_public_key_path, $sign)
  391. {
  392. $pubKey = file_get_contents($ali_public_key_path);
  393. $res = openssl_get_publickey($pubKey);
  394. if (!$res)
  395. {
  396. throw new Exception('公钥格式错误');
  397. }
  398. $result = (bool) openssl_verify($data, base64_decode($sign), $res);
  399. openssl_free_key($res);
  400. return $result;
  401. }
  402. /**
  403. * RSA解密
  404. * @param $content string 需要解密的内容,密文
  405. * @param $private_key_path string 商户私钥文件路径
  406. * @return string 解密后内容,明文
  407. */
  408. function rsaDecrypt($content, $private_key_path)
  409. {
  410. $priKey = file_get_contents($private_key_path);
  411. $res = openssl_get_privatekey($priKey);
  412. //用base64将内容还原成二进制
  413. $content = base64_decode($content);
  414. //把需要解密的内容,按128位拆开解密
  415. $result = '';
  416. for ($i = 0; $i < strlen($content) / 128; $i++)
  417. {
  418. $data = substr($content, $i * 128, 128);
  419. openssl_private_decrypt($data, $decrypt, $res);
  420. $result .= $decrypt;
  421. }
  422. openssl_free_key($res);
  423. return $result;
  424. }
  425. }