近期公司的APP打算上線,需要整合支付的功能。由於採用的是Python進行開發,因此無法直接使用官方提供的SDK。雖然也有一些整合的第3方可以使用,比如ping++、beecloud。
但是由於提供的時間比較充裕,於是就自己實現了1個。在這個過程中,難免遇到一些坑,而這些坑有時會困擾你很久。
最初,並沒有打算寫這麼一篇文章,因為它的適用範圍很窄。但是網上搜尋到的關於APP支付方面的都是移動端iOS和Android的實現方式,對於服務端的實現寥寥無幾。相比而言,python在當前畢竟是小眾語言,而如果參考其他語言,比如php的實現,發現這個過程還是有不少地方是沒有講清楚的。
雖然對於很多開發者來說,支付這個功能涉及的知識點並不是很多,但是你會發現你卻在這裡耗費了很多的時間。有時1個簽名的問題,就讓你無法呼叫支付,比如支付寶的Alipay10
問題,總是出現伺服器繁忙的提示,其實就是你的簽名出了問題。
在這裡,由於涉及到公司的一些敏感資訊的問題,因此下面程式碼中的簽名用的都是測試資料,而簽名是根據已經驗證通過的函式呼叫計算出來的。當你發現自己簽名不過時,可以直接複製這些字串,然後比對下面計算出來的簽名來檢視你的簽名函式及你的回撥處理哪裡出了問題。
適用範圍
首先為了避免耽誤大家的時間,這裡我們只實現了微信支付及支付寶的移動支付。對於微信公眾支付及支付寶的其他支付場景是不適用的。
這裡,限於篇幅,只對訂單支付及非同步回撥的部分進行說明,因為如果把所有的介面都過一遍,太耗費時間,還不如直接在pypi上上傳1個包,直接使用pip安裝。
在這裡,將用到的簽名的方式單獨提取出來進行講解,對於相同產品其他的介面也是適用的,只是請求的引數有所變化而已。
個人建議及使用的庫
在正式講述APP支付之前,我有如下的建議:
- Python版本>=2.7.9,由於Python版本2.7.9為1個bug修復版本,在這個版本中使用新的SSL模組,修復了之前HTTP客戶端模組(比如urllib2,httplib)不對伺服器證照進行校驗的問題,詳情請檢視PEP 476。
- 使用lxml,而不是標準庫中的XML庫,主要在於標準庫中的XML模組無法檢驗惡意構造的資料,詳情請檢視Warning。
- 使用pycrypto庫用於支付寶RSA簽名,版本>=2.61。這裡使用的是pycrypto,是因為安裝比較方便,另外因為版本2.61之前在某種情況下,使用fork會出現隨機數不安全的問題,詳情請檢視CVE-2013-1445。
職責
下面我們需要理清我們要做的事情,避免不必要的工作。主要是如下2個方面:
- 服務端負責生成訂單及簽名,及接受支付非同步通知
- 客戶端負責使用服務端傳來的訂單資訊呼叫支付介面,及根據SDK同步返回的支付結果展示結果頁。
另外,私鑰必須放在服務端,簽名過程也必須放在服務端。
支付方式比較
共同點
在這2種支付方式中,我們需要對簽名的資訊(URL鍵值對,例如key1=value1&key2=valu2…)按照ASCII編碼順序進行排序後再進行簽名,並且採用POST方式進行提交。
不同點
- 在微信中,簽名的方式採用的是md5,而支付寶採用的RSA。
- 在微信支付中,提交和返回資料都為XML格式,其根節點為xml。而在支付寶中,採用的是使用表單提交的方式來進行。
- 由於微信支付採用的是XML格式,因此字元編碼採用的是UTF-8,而支付寶需要指定引數
_input_charset
來指定編碼,官方建議我們採用UTF-8。
下面我們正式進行APP支付流程的說明,在這個過程中,我們需要閱讀官方提供的文件。這裡我們從微信開始,因為相比支付寶,微信的支付呼叫更為簡單些。
微信
在進行模組程式碼編寫之前,我們來看看官方提供的流程圖。換句話說,在我們呼叫統一下單介面後,我們需要給APP客戶端返回prepayid
及生成的簽名,另外還有APP端調起支付介面中的其他欄位。
統一下單
這裡,假設我們統一下單時請求引數如下:
1 |
appid=wx2421b1c4370ec43b&attach=支付測試&body=APP支付測試&mch_id=10000100&nonce_str=1add1a30ac87aa2db72f57a2375d8fec¬ify_url=http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php&out_trade_no=1415659990&spbill_create_ip=14.23.150.211&total_fee=1&trade_type=APP |
而我們的商戶號假設為1900000109
,那麼我們需要將商戶號與之前的請求引數拼接在一起:
1 2 3 4 |
data = 'appid=wx2421b1c4370ec43b&attach=支付測試&body=APP支付測試&mch_id=10000100&nonce_str=1add1a30ac87aa2db72f57a2375d8fec¬ify_url=http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php&out_trade_no=1415659990&spbill_create_ip=14.23.150.211&total_fee=1&trade_type=APP&key=1900000109' >>> from hashlib import md5 >>> md5(data).hexdigest().upper() 'F3D12D07612100A7F0DA652E97A766FA' |
這裡我們拼接後的引數進行MD5加密後將其轉換為大寫字母,這樣就得到我們需要的簽名了。因此,在請求統一下單時,我們需要傳遞如下的字串:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<xml> <appid>wx2421b1c4370ec43b</appid> <attach>支付測試</attach> <body>APP支付測試</body> <mch_id>10000100</mch_id> <nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str> <notify_url>http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php</notify_url> <out_trade_no>1415659990</out_trade_no> <spbill_create_ip>14.23.150.211</spbill_create_ip> <total_fee>1</total_fee> <trade_type>APP</trade_type> <sign>F3D12D07612100A7F0DA652E97A766FA</sign> </xml> |
關於簽名校驗,微信官方提供了1個校驗工具,當在請求返回的err_code
出現SIGNERROR
時可以使用這個工具來輔助我們進行校驗。
返回給客戶端APP
當我們成功請求統一下單介面後,返回的結果可能如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str> <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign> <result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id> <trade_type><![CDATA[APP]]></trade_type> </xml> |
接下來,我們需要取出返回結果中的prepay_id
引數,然後按照調起支付介面中組裝請求引數,假設我們得到如下的請求引數:
1 |
appid=wx2421b1c4370ec43b&noncestr=5K8264ILTKCH16CQ2&package=Sign=WXPay&partnerid=1900000109&prepayid=wx201411101639507cbf6ffd8b0779950874×tamp=1412000000 |
那麼進行簽名後將得到字串0586C6E4A2AA6D297F4046362D878BAC
。那麼我們返回給客戶端APP的欄位主要有prepayid
、noncestr
、timestamp
、sign
。
非同步回撥
當使用者成功完成支付後,微信會將相關支付資訊推送到在統一下單時提交的notify_url
指定的url地址中。在這一步,我們主要要做的是檢驗資訊,比如簽名是否正確、支付金額是否相同,可以在這個過程中修改訂單的支付狀態。
如果檢驗通過後,我們需要給微信返回類似如下的引數:
1 2 3 4 |
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> </xml> |
在這一步可能遇到的問題是無法接收到微信推送過來的引數,由於這裡公司採用的是Flask,因此採用如下的方式來進行接收:
1 2 3 4 5 6 7 8 |
from flask import request ... @app.route('/notify') def notify(): req = request.stream.read() ... |
在這裡,我採用的是從原始流中進行直接讀取操作。
說完了微信,我們來看下支付寶的情況。
支付寶
這裡我採用UTF-8編碼進行處理,並檢視如下的功能流程,讓我們對支付流程有1個瞭解。
準備
在正式開始支付寶的支付之前,我們先來說下基礎的一些內容,首先是要使用的私鑰要是PKCS8格式的。然後是需要傳遞給支付寶的引數,其中基本引數partner
、_input_charset
、sign
、sign_type
、service
這些屬於基本引數,是必須要傳遞的引數。關於需要傳遞引數的內容請查閱引數。
支付待簽名字串生成
關於支付請求引數,我們可以檢視下面的連結請求引數。
在支付寶APP支付中,我們需要請求引數中需要剔除sign_type
、sign
這2個引數,並在簽名之前需要對字串進行UTF-8的urlencode,即待簽名字串如果有中文則必須未轉義形式顯示,例如:
1 |
_input_charset="utf-8"¬ify_url="http://notify.msp.hk/notify.htm"&out_trade_no="0819145412-6177"&partner="2088101568338364"&seller_id="xxx@alipay.com"&service="mobile.securitypay.pay"&subject="測試"&payment_type="1"&total_fee="0.01" |
在這裡,我們對請求的引數進行了排序,然後請求的引數的數值需要新增雙引號。之後,我們需要對上面的字串進行簽名處理,這裡我們假設我們的私鑰如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDQ3/XlPY/IFw8FISXKHVRLICPSEPmWCauMtKPoAc9M6szlCjG+ YqtxaigPwVdRqoG3m24uMgz36qXyANvXMB3X7e6t6g1DoI3wxy5aNNlE0Dlu0BIH rcLUFsSZgCTuAvOori2oGVp6StXz0Wg5kacICnf6GNHCM1B2IgshEQte2wIDAQAB AoGAMkbmanKiDFi4jdSHwxnCM38eAC+D1ECpoWnN1kexPWN7RFpq1NftSpRx5jD0 srynEqoAIHB9vKMnpJPeVvLHC8ZvtZyehQPTvdaqdeORcZUhaYHYBWgiCCr/6fgW 00yxR+UrYZFY6DEHbHkXgXqtEFzoVYIVwI6a90F/xFQ8hpECQQDoypOny/zUvocc hTQ/JuqsmZXKNZgU+1c/3Kflz7RDpi9e94yR9eaBSLBTDEkngJkJD5/riTzC0O4A Hb/2+5vzAkEA5bL5lgoCWyyVlvy/PBbZ2Ilcf+vMyvtyDBWklW9xrXEy53W+G4Qq NSatTzNHN2VNEqFz2/3xNIbFlMpHzU3zeQJBAJS3thTgkKko/xANWQ9vQUT66WLB UmM1HsxBn1GFm9gL9v9ojnlA6v10/pBPrPx7f0j2nmfOyO58o0+XseeLXlkCQB55 k2GTrGJaVPJ2UAzx3y86cjpKl54qpCP0TyTAZ22igiVxWqqd61en7QCABifUWdhp 8UwzsefNJbOq7sHPYMkCQACbuh1TKx9AlZz1kPoAagBsZofx4cb5QnHpmIzREbRd aydfoaqR5BKpjJXky4tyBDeyp50s96UUd/eEYDC8RV4= -----END RSA PRIVATE KEY----- |
在RSA簽名就驗證簽名中,我們需要確保公鑰和私鑰都包含在BEGIN和END之間,且不需要進行將其放在1行中。
然後我們使用如下的方式進行簽名操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from Crypto.Signature import PKCS1_v1_5 from Crypto.Hash import SHA from Crypto.PublicKey import RSA from base64 import b64encode message = '_input_charset="utf-8"¬ify_url="http://notify.msp.hk/notify.htm"&out_trade_no="0819145412-6177"&partner="2088101568338364"&seller_id="xxx@alipay.com"&service="mobile.securitypay.pay"&subject="測試"&payment_type="1"&total_fee="0.01"' key = RSA.importKey(open('rsa_private_key.pem').read()) h = SHA.new() h.update(message) signer = PKCS1_v1_5.new(key) signature = signer.sign(h) print b64encode(signature) |
這樣我們將得到簽名:
1 |
FDW1YrI/FeX841orIDZ+rYyacSyDtWs4d+GPNpEMbWd38TpmePLagEIzAd8DDB3TlLxwyiA/IgGYIiLPQOk8qdIdp3AkjWHEMPmRbULZx2bMVNJlJy/yunOAbJRIJhP3I1Ip/nCFRVvBmBE3I8Mt95UQtYhtLkx+fZbuXmpCckQ= |
在這裡,官方所說的是SHAWithRSA函式對應於PKCS1_V1_5
標準外加SHA1加密方式,需要主要的是這裡生成的私鑰的長度是1024位。
然後我們對引數字串進行拼接將得到:
1 |
_input_charset="utf-8"¬ify_url="http://notify.msp.hk/notify.htm"&out_trade_no="0819145412-6177"&partner="2088101568338364"&seller_id="xxx@alipay.com"&service="mobile.securitypay.pay"&subject="測試"&payment_type="1"&total_fee="0.01"&sign="FDW1YrI/FeX841orIDZ+rYyacSyDtWs4d+GPNpEMbWd38TpmePLagEIzAd8DDB3TlLxwyiA/IgGYIiLPQOk8qdIdp3AkjWHEMPmRbULZx2bMVNJlJy/yunOAbJRIJhP3I1Ip/nCFRVvBmBE3I8Mt95UQtYhtLkx+fZbuXmpCckQ="&sign_type="RSA" |
我們將生成的這串字串返回給客戶端APP呼叫即可。
非同步回撥
與微信一樣,當使用者成功支付後,支付寶會主動以POST方式將資料推送給你提交的notify_url
中的URL。在這裡,我們需要以表單的形式來接收傳遞過來的引數。
在此之前,我們說下一些關於通知的內容:
- 通知觸發條件:支付寶只有在交易成功、支付成功以及交易建立是會觸發通知,對於交易關閉時不觸發通知的,換句話說在這些情況下會主動推送訊息給你。
- 通知交易狀態:主要有4種狀態,
TRADE_SUCCESS
,TRADE_FINISHED
、TRADE_CLOSED
,WAIT_BUYER_PAY
,分別對應交易成功、交易完成、交易關閉和等待買家付款。
而支付寶會傳遞過來的引數,我們可以檢視伺服器非同步通知引數。
在非同步回撥中,我們需要完成如下2個驗證的工作:
- 驗證簽名
- 驗證是否是支付寶發來的通知
對於第2個驗證,我們需要拼裝成如下的URL:
1 |
https://mapi.alipay.com/gateway.do?service=notify_verify&partner=2088002396712354¬ify_id=RqPnCoPT3K9%252Fvwbh3I%252BFioE227%252BPfNMl8jwyZqMIiXQWxhOCmQ5MQO%252FWd93rvCB%252BaiGg |
然後我們進行GET請求,而結果會返回1個true或false的字串。
對於第1種驗證,假設我們有如下的字串:
1 |
discount=0.00&payment_type=8&subject=測試&trade_no=2013082244524842&buyer_email=dlwdgl@gmail.com&gmt_create=2013-08-22 14:45:23¬ify_type=trade_status_sync&quantity=1&out_trade_no=082215222612710&seller_id=2088501624816263¬ify_time=2013-08-22 14:45:24&body=測試測試&trade_status=TRADE_SUCCESS&is_total_fee_adjust=N&total_fee=1.00&gmt_payment=2013-08-22 14:45:24&seller_email=xxx@alipay.com&price=1.00&buyer_id=2088602315385429¬ify_id=64ce1b6ab92d00ede0ee56ade98fdf2f4c&use_coupon=N&sign_type=RSA&sign=1glihU9DPWee+UJ82u3+mw3Bdnr9u01at0M/xJnPsGuHh+JA5bk3zbWaoWhU6GmLab3dIM4JNdktTcEUI9/FBGhgfLO39BKX/eBCFQ3bXAmIZn4l26fiwoO613BptT44GTEtnPiQ6+tnLsGlVSrFZaLB9FVhrGfipH2SWJcnwYs= |
我們剔除了sign
和sign_type
引數後,按照ASCII順序進行排序,我們將得到如下的字串:
1 |
body=測試測試&buyer_email=dlwdgl@gmail.com&buyer_id=2088602315385429&discount=0.00&gmt_create=2013-08-22 14:45:23&gmt_payment=2013-08-22 14:45:24&is_total_fee_adjust=N¬ify_time=2013-08-22 14:45:24¬ify_type=trade_status_sync&out_trade_no=082215222612710&payment_type=8&price=1.00&quantity=1&seller_email=alipayrisk18@alipay.com&seller_id=2088501624816263&subject=測試&total_fee=1.00&trade_no=2013082244524842&trade_status=TRADE_SUCCESS&use_coupon=N |
然後我們進行如下的驗證簽名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from Crypto.Signature import PKCS1_v1_5 from Crypto.Hash import SHA from Crypto.PublicKey import RSA from base64 import b64decode sign = '1glihU9DPWee+UJ82u3+mw3Bdnr9u01at0M/xJnPsGuHh+JA5bk3zbWaoWhU6GmLab3dIM4JNdktTcEUI9/FBGhgfLO39BKX/eBCFQ3bXAmIZn4l26fiwoO613BptT44GTEtnPiQ6+tnLsGlVSrFZaLB9FVhrGfipH2SWJcnwYs=' msg = 'body=測試測試&buyer_email=dlwdgl@gmail.com&buyer_id=2088602315385429&discount=0.00&gmt_create=2013-08-22 14:45:23&gmt_payment=2013-08-22 14:45:24&is_total_fee_adjust=N¬ify_time=2013-08-22 14:45:24¬ify_type=trade_status_sync&out_trade_no=082215222612710&payment_type=8&price=1.00&quantity=1&seller_email=alipayrisk18@alipay.com&seller_id=2088501624816263&subject測試&total_fee=1.00&trade_no=2013082244524842&trade_status=TRADE_SUCCESS&use_coupon=N' key = RSA.importKey(open('alipay_public_key.pem').read()) sign = b64decode(sign) h = SHA.new(msg) verifier = PKCS1_v1_5.new(key) print verifier.verify(h,sign) |
在這裡,我們讀取支付寶的公鑰,然後對簽名進行base64編碼解密,然後進行比對操作,其結果為1個布林值。
最後,如果2個檢驗都通過,我們需要返回給支付寶1個字串success即可。
參考文章:
https://doc.open.alipay.com/d…
https://doc.open.alipay.com/d…