支付寶在所有支付方式中最好開發的了,因為文件比較清晰,而且開發起來也比較簡單。因此,支付寶的坑是相對較少的。
原文地址
APP支付
APP支付步驟為:
- 獲取支付寶的配置資訊。
- 生成商家訂單資訊。
- 根據訂單資訊生成待校驗資料。
- 生成請求給支付寶的加密字串。
- 將待校驗資料和加密字串拼接,返回給APP。
- APP將得到的資料請求支付寶客戶端進行支付。
由於APP支付是由APP去調起支付寶支付,所以服務端需要做的事情就是將請求引數封裝好之後返回APP即可。
-
獲取支付寶的配置資訊。
支付時需要的配置資訊有:- key: 交易安全校驗碼。
- app_id:支付寶分配給開發者的應用ID。
-
生成商家訂單資訊。
這個步驟由商家自行生成。支付寶那邊只需要知道的訂單資訊為:- subject: 必填。商品的標題/交易標題/訂單標題/訂單關鍵字等。
- total_amount: 必填。訂單價格。
- out_trade_no: 必填。商戶網站唯一訂單號。
- body: 非必填。交易的具體描述資訊。
- 根據訂單資訊生成待校驗資料。
APP支付的詳細請求引數: 點選檢視
-
生成請求給支付寶的加密字串。
$sign = $alipaySubmit->buildRequestParaForApp($para_token);
其中,
buildRequestParaForApp
的實現為:- 對待簽名引數陣列排序
/** * 對陣列排序 * @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將得到的資料請求支付寶客戶端進行支付。
APP端將拼接好的字串拿去請求支付寶客戶端即可調起支付寶進行支付。拼接好的字串大致如下圖所示:
網頁版支付
網頁版支付步驟為:
- 設定支付寶的配置資訊。
- 向支付寶申請新訂單,獲取支付token。
- 攜帶token進行訂單支付。
網頁版的支付寶支付相對於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。
請求token的service為:
alipay.wap.trade.create.direct
。-
構造引數:
$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 排序前的陣列
-
*/
function argSort($para) {
ksort($para);
reset($para);
return $para;
}
```
簽名:
```php
/**
* 生成簽名結果
* @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;
}
```
3. 用構造好的引數請求支付寶後臺申請新訂單:
**注意:請求時,必須帶上SSL證照。**
```php
$sResult = getHttpResponsePOST($this->alipay_gateway_new, $this->alipay_config['cacert'],$request_data,trim(strtolower($this->alipay_config['input_charset'])));
```
請求函式的實現:
```php
/**
* 遠端獲取資料,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。
```php
//URLDECODE返回的資訊
$html_text = urldecode($html_text);
//解析遠端模擬提交後返回的資訊
$para_html_text = parseResponse($html_text);
//獲取request_token
$request_token = $para_html_text['request_token'];
```
parseResponse函式的實現:
```php
/**
* 解析遠端模擬提交後返回的資訊
* @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;
}
```
-
攜帶token進行訂單支付。
成功請求token回來後,就可以向支付寶發出一次支付請求。
同樣構造請求資料:
//業務詳細只需要攜帶步驟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'])) );
將這些引數,在頁面中傳送給支付寶即可發起一次支付請求。
在PHP 中的實現就是將這些引數,渲染至HTML中,再將HTML中的表單提交即可。
到此,網頁版的支付寶支付完成整個流程。
支付結果非同步通知
在上面,我們看到有兩個引數傳給了支付寶:
-
call_back_url
: 交易成功後,支付寶頁面上“返回到商家頁面”的地址(同步回撥) -
notify_url
: 交易狀態變更後,支付寶通知網站的回撥地址(非同步通知)
對於手機網站支付產生的交易,支付寶會根據原始支付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;
}
-
獲取支付寶通知資料
支付寶非同步通知是POST請求,返回的資料結構如下:{ "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" }
-
驗籤資料
驗籤需要支付寶的公鑰驗籤和簽名的流程是一樣的,都是將所有除了
sign
以外的引數,進行字典排序,並以key=value
的形式以&
符號拼成字串,再使用金鑰進行簽名,將得到的簽名與支付寶返回的簽名進行對比,完成驗簽過程。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; }
但是這裡有個坑,就是返回資料中的
fund_bill_list
是經過html轉義的(如例子中的資料:[{"amount":"31.00","fundChannel":"ALIPAYACCOUNT"}]
),如果直接使用該引數進行簽名,則會導致簽名失敗。這裡就需要將字串轉義了:[{"amount":"31.00","fundChannel":"ALIPAYACCOUNT"}]
,用轉義後的引數值進行簽名,通過校驗。 - 更改訂單狀態
驗簽完畢後,後臺就可以根據實際情況進行訂單狀態的更改。
完畢
祝各位程式猿在開發支付寶支付時不再有坑,也希望支付寶在後續的更新中不再埋雷。