主要针对在App应用和网页版的支付功能(支付宝,微信,银联)开发时,所遇到的坑。能填则填。
支付宝在所有支付方式中最好开发的了,因为文档比较清晰,而且开发起来也比较简单。因此,支付宝的坑是相对较少的。
APP支付步骤为:
由于APP支付是由APP去调起支付宝支付,所以服务端需要做的事情就是将请求参数封装好之后返回APP即可。
生成请求给支付宝的加密字符串。
$sign = $alipaySubmit->buildRequestParaForApp($para_token);
/** * 对数组排序 * @param $para 排序前的数组 * return 排序后的数组 */ function argSort($para) { ksort($para); reset($para); return $para; }
生成签名结果(阿里推荐的是RSA2的签名方式,这里项目用的是RSA)
/** * RSA签名 * @param $data 待签名数据 * @param $private_key_path 商户私钥文件路径 * return 签名结果 */ function rsaSign($data, $private_key_path) { $priKey = file_get_contents($private_key_path); $res = openssl_get_privatekey($priKey); openssl_sign($data, $sign, $res); openssl_free_key($res); //base64编码 $sign = base64_encode($sign); return $sign; }
将待校验数据和加密字符串拼接,返回给APP。
$url = ""; foreach ($para_token as $key => $value) { $url .= $key."=".urlencode($value)."&"; } return $url."sign=".urlencode($sign);
网页版支付步骤为:
网页版的支付宝支付相对于APP调起支付宝要复杂,因为网页支付时,需要多次请求支付宝服务器获取支付的必要参数。
/**调用授权接口alipay.wap.trade.create.direct获取授权码token**/ //返回格式 private $format = ""; //必填,不需要修改 //版本 private $v = ""; //必填,不需要修改 //请求号 private $req_id = ""; //必填,须保证每次请求都是唯一 //**req_data详细信息** //服务器异步通知页面路径 private $notify_url = ""; //需http://格式的完整路径,不允许加?id=123这类自定义参数 //页面跳转同步通知页面路径 private $call_back_url = ""; //需http://格式的完整路径,不允许加?id=123这类自定义参数 //卖家支付宝账户 private $seller_email = ""; //必填 //商户订单号 private $out_trade_no = ""; //商户网站订单系统中唯一订单号,必填 //订单名称 private $subject = ""; //必填 //付款金额 private $total_fee = ""; //必填 //请求业务参数详细 private $req_data = ""; //必填 //配置 private $alipay_config = array(); /************************************************************/
向支付宝申请新订单,并获取订单的token。
1.请求token的service为: alipay.wap.trade.create.direct。
2.构造参数:
$para_token = array( "service" => "alipay.wap.trade.create.direct", // 合作者身份(partner ID) "partner" => trim($this->alipay_config['partner']), // APP使用的是RSA,网页版使用的是MD5 "sec_id" => trim($this->alipay_config['sign_type']), // 返回的数据格式 "format" => $this->format, // 版本号? "v" => $this->v, // 唯一的请求号 "req_id" => $this->req_id, // 请求参数 "req_data" => $req_data, // 字符集,一般为utf8即可。 "_input_charset" => trim(strtolower($this->alipay_config['input_charset'])) );
将构造好的请求参数,进行处理,字典排序,拼接字符串,签名:
$para_filter = paraFilter($para_temp); $para_sort = argSort($para_filter); $mysign = $this->buildRequestMysign($para_sort); //签名结果与签名方式加入请求提交参数组中 $para_sort['sign'] = $mysign; return $para_sort;
处理:过滤值为空的数据,过滤签名类型和签名。
function paraFilter($para) { $para_filter = array(); while (list ($key, $val) = each ($para)) { if($key == "sign" || $key == "sign_type" || $val == "")continue; else $para_filter[$key] = $para[$key]; } return $para_filter; }
/** * 对数组排序 * @param $para 排序前的数组 * return 排序后的数组 */ function argSort($para) { ksort($para); reset($para); return $para; }
签名:
/** * 生成签名结果 * @param $para_sort 已排序要签名的数组 * return 签名结果字符串 */ function buildRequestMysign($para_sort) { //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 $prestr = createLinkstring($para_sort); $mysign = ""; switch (strtoupper(trim($this->alipay_config['sign_type']))) { case "MD5" : // MD5直接将密钥拼接在字符串后面再进行MD5加密。 $mysign = md5Sign($prestr, $this->alipay_config['key']); break; case "RSA" : // RSA则是先读取商户的私钥,再用该密钥对字符串进行加密。 $mysign = rsaSign($prestr, $this->alipay_config['private_key_path']); break; case "0001" : $mysign = rsaSign($prestr, $this->alipay_config['private_key_path']); break; default : $mysign = ""; } return $mysign; }
$sResult = getHttpResponsePOST($this->alipay_gateway_new, $this->alipay_config['cacert'],$request_data,trim(strtolower($this->alipay_config['input_charset'])));
请求函数的实现:
/** * 远程获取数据,POST模式 * 注意: * 1.使用Crul需要修改服务器中php.ini文件的设置,找到php_curl.dll去掉前面的";"就行了 * 2.文件夹中cacert.pem是SSL证书请保证其路径有效,目前默认路径是:getcwd().'\cacert.pem' * @param $url 指定URL完整路径地址 * @param $cacert_url 指定当前工作目录绝对路径 * @param $para 请求的数据 * @param $input_charset 编码格式。默认值:空值 * return 远程输出的数据 */ function getHttpResponsePOST($url, $cacert_url, $para, $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,$cacert_url);//证书地址 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,$para);// post传输数据 $responseText = curl_exec($curl); //var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容 curl_close($curl); return $responseText; }
处理支付宝返回的数据,并获取token。
//URLDECODE返回的信息 $html_text = urldecode($html_text); //解析远程模拟提交后返回的信息 $para_html_text = parseResponse($html_text); //获取request_token $request_token = $para_html_text['request_token'];
/** * 解析远程模拟提交后返回的信息 * @param $str_text 要解析的字符串 * @return 解析结果 */ function parseResponse($str_text) { //以“&”字符切割字符串 $para_split = explode('&',$str_text); //把切割后的字符串数组变成变量与数值组合的数组 foreach ($para_split as $item) { //获得第一个=字符的位置 $nPos = strpos($item,'='); //获得字符串长度 $nLen = strlen($item); //获得变量名 $key = substr($item,0,$nPos); //获得数值 $value = substr($item,$nPos+1,$nLen-$nPos-1); //放入数组中 $para_text[$key] = $value; } if( ! empty ($para_text['res_data'])) { //解析加密部分字符串 if($this->alipay_config['sign_type'] == '0001') { $para_text['res_data'] = rsaDecrypt($para_text['res_data'], $this->alipay_config['private_key_path']); } //token从res_data中解析出来(也就是说res_data中已经包含token的内容) $doc = new DOMDocument(); $doc->loadXML($para_text['res_data']); $para_text['request_token'] = $doc->getElementsByTagName( "request_token" )->item(0)->nodeValue; } return $para_text; }
//业务详细只需要携带步骤2的token即可。 $req_data = '<auth_and_execute_req><request_token>' . $request_token . '</request_token></auth_and_execute_req>'; //必填 //构造要请求的参数数组,无需改动 $parameter = array( "service" => "alipay.wap.auth.authAndExecute", // 合作者身份(partner ID) "partner" => trim($this->alipay_config['partner']), // 签名类型 "sec_id" => trim($this->alipay_config['sign_type']), // 和步骤2一致 "format" => $this->format, "v" => $this->v, "req_id" => $this->req_id, // 业务详细参数 "req_data" => $req_data, // 字符集,一般为utf8. "_input_charset" => trim(strtolower($this->alipay_config['input_charset'])) );
在上面,我们看到有两个参数传给了支付宝:
对于手机网站支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。
对于App支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。
支付宝异步通知官方文档中写的比较清楚,什么时候出发通知,返回什么参数,注意事项都有,开发者可以根据自己的情况查看具体信息。
验签步骤可以移步至这里
这里就简单的用手上的项目举例说明,支付宝通知后,后台是如何进行验签和处理订单。
public function app_notifyOp(){ $payment_api = $this->_get_payment_api(); $payment_config = $this->_get_payment_config(); // 支付宝是用POST方式发送通知信息 $callback_info = $payment_api->getNotifyInfoApp($_POST); if($callback_info) { //验证成功 if ($callback_info['order_state']) { // 如果是支付成功则改变订单状态 $result = $this->_update_order($callback_info['out_trade_no'], $callback_info['trade_no']); }else{ // 如果是退款成功则修改退订的相关状态 $result = $this->_app_refund($callback_info['out_trade_no'], $callback_info['trade_no'], $callback_info['refund_fee']); } if($result['state']) { echo 'success';die; } } //验证失败 echo "fail";die; }
{ "total_amount": "31.00", "buyer_id": "ID", "trade_no": "TRADE_NO", "body": "pay_sn:580546601841783375", "notify_time": "2017-04-27 09:50:59", "subject": "580546601841783375", "sign_type": "RSA", "buyer_logon_id": "ID", "auth_app_id": "APPID", "charset": "utf-8", "notify_type": "trade_status_sync", "invoice_amount": "31.00", "out_trade_no": "580546601841783375_r", "trade_status": "TRADE_SUCCESS", "gmt_payment": "2017-04-27 09:50:58", "version": "1.0", "point_amount": "0.00", "sign": "SIGNATURE", "gmt_create": "2017-04-27 09:50:58", "buyer_pay_amount": "31.00", "receipt_amount": "31.00", "fund_bill_list": "[{"amount":"31.00","fundChannel":"ALIPAYACCOUNT"}]", "app_id": "APPID", "seller_id": "SELLERID", "notify_id": "8414394a1190f25edbbec9ba4b98642mem", "seller_email": "YOUR_ALIPAY_ACCOUNT" }
function getSignVeryfy($para_temp, $sign) { //除去待签名参数数组中的空值和签名参数 $para = paraFilter($para_temp); //对待签名参数数组排序 $para = argSort($para); //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 $prestr = createLinkstring($para); $prestr = htmlspecialchars_decode($prestr); $isSgin = false; switch (strtoupper(trim($this->alipay_config['sign_type']))) { case "MD5" : $isSgin = md5Verify($prestr, $sign, $this->alipay_config['key']); break; case "RSA" : $isSgin = rsaVerify($prestr, trim($this->alipay_config['ali_public_key_path']), $sign); break; case "0001" : $isSgin = rsaVerify($prestr, trim($this->alipay_config['ali_public_key_path']), $sign); break; default : $isSgin = false; } logResult($log); return $isSgin; }
点赞+转发,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)
关注 {我},享受文章首发体验!
每周重点攻克一个前端技术难点。更多精彩前端内容私信 我 回复“教程”