玩轉APP支付

原文地址:http://52sox.com/python-play-...php

近期公司的APP打算上線,須要集成支付的功能。因爲採用的是Python進行開發,所以沒法直接使用官方提供的SDK。雖然也有一些集成的第3方可使用,好比ping++beecloud
可是因爲提供的時間比較充裕,因而就本身實現了1個。在這個過程當中,不免遇到一些坑,而這些坑有時會困擾你好久。
最初,並無打算寫這麼一篇文章,由於它的適用範圍很窄。可是網上搜索到的關於APP支付方面的都是移動端iOS和Android的實現方式,對於服務端的實現寥寥無幾。相比而言,python在當前畢竟是小衆語言,而若是參考其餘語言,好比php的實現,發現這個過程仍是有很多地方是沒有講清楚的。
雖然對於不少開發者來講,支付這個功能涉及的知識點並非不少,可是你會發現你卻在這裏耗費了不少的時間。有時1個簽名的問題,就讓你沒法調用支付,好比支付寶的Alipay10問題,老是出現服務器繁忙的提示,其實就是你的簽名出了問題。
在這裏,因爲涉及到公司的一些敏感信息的問題,所以下面代碼中的簽名用的都是測試數據,而簽名是根據已經驗證經過的函數調用計算出來的。當你發現本身簽名不過期,能夠直接複製這些字符串,而後比對下面計算出來的簽名來查看你的簽名函數及你的回調處理哪裏出了問題。html

適用範圍

首先爲了不耽誤你們的時間,這裏咱們只實現了微信支付及支付寶的移動支付。對於微信公衆支付及支付寶的其餘支付場景是不適用的。
這裏,限於篇幅,只對訂單支付及異步回調的部分進行說明,由於若是把全部的接口都過一遍,太耗費時間,還不如直接在pypi上上傳1個包,直接使用pip安裝。
在這裏,將用到的簽名的方式單獨提取出來進行講解,對於相同產品其餘的接口也是適用的,只是請求的參數有所變化而已。python

我的建議及使用的庫

在正式講述APP支付以前,我有以下的建議:flask

  1. Python版本>=2.7.9,因爲Python版本2.7.9爲1個bug修復版本,在這個版本中使用新的SSL模塊,修復了以前HTTP客戶端模塊(好比urllib2,httplib)不對服務器證書進行校驗的問題,詳情請查看PEP 476api

  2. 使用lxml,而不是標準庫中的XML庫,主要在於標準庫中的XML模塊沒法檢驗惡意構造的數據,詳情請查看Warning安全

  3. 使用pycrypto庫用於支付寶RSA簽名,版本>=2.61。這裏使用的是pycrypto,是由於安裝比較方便,另外由於版本2.61以前在某種狀況下,使用fork會出現隨機數不安全的問題,詳情請查看CVE-2013-1445服務器

職責

下面咱們須要理清咱們要作的事情,避免沒必要要的工做。主要是以下2個方面:微信

  • 服務端負責生成訂單及簽名,及接受支付異步通知app

  • 客戶端負責使用服務端傳來的訂單信息調用支付接口,及根據SDK同步返回的支付結果展現結果頁。異步

另外,私鑰必須放在服務端,簽名過程也必須放在服務端。

支付方式比較

共同點

在這2種支付方式中,咱們須要對簽名的信息(URL鍵值對,例如key1=value1&key2=valu2...)按照ASCII編碼順序進行排序後再進行簽名,而且採用POST方式進行提交。

不一樣點

  1. 在微信中,簽名的方式採用的是md5,而支付寶採用的RSA。

  2. 在微信支付中,提交和返回數據都爲XML格式,其根節點爲xml。而在支付寶中,採用的是使用表單提交的方式來進行。

  3. 因爲微信支付採用的是XML格式,所以字符編碼採用的是UTF-8,而支付寶須要指定參數_input_charset來指定編碼,官方建議咱們採用UTF-8。

下面咱們正式進行APP支付流程的說明,在這個過程當中,咱們須要閱讀官方提供的文檔。這裏咱們從微信開始,由於相比支付寶,微信的支付調用更爲簡單些。

微信

在進行模塊代碼編寫以前,咱們來看看官方提供的流程圖。換句話說,在咱們調用統一下單接口後,咱們須要給APP客戶端返回prepayid及生成的簽名,另外還有APP端調起支付接口中的其餘字段。

統一下單

這裏,假設咱們統一下單時請求參數以下:

appid=wx2421b1c4370ec43b&attach=支付測試&body=APP支付測試&mch_id=10000100&nonce_str=1add1a30ac87aa2db72f57a2375d8fec&notify_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,那麼咱們須要將商戶號與以前的請求參數拼接在一塊兒:

data = 'appid=wx2421b1c4370ec43b&attach=支付測試&body=APP支付測試&mch_id=10000100&nonce_str=1add1a30ac87aa2db72f57a2375d8fec&notify_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加密後將其轉換爲大寫字母,這樣就獲得咱們須要的簽名了。所以,在請求統一下單時,咱們須要傳遞以下的字符串:

<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

當咱們成功請求統一下單接口後,返回的結果可能以下所示:

<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參數,而後按照調起支付接口中組裝請求參數,假設咱們獲得以下的請求參數:

appid=wx2421b1c4370ec43b&noncestr=5K8264ILTKCH16CQ2&package=Sign=WXPay&partnerid=1900000109&prepayid=wx201411101639507cbf6ffd8b0779950874&timestamp=1412000000

那麼進行簽名後將獲得字符串0586C6E4A2AA6D297F4046362D878BAC。那麼咱們返回給客戶端APP的字段主要有prepayidnoncestrtimestampsign

異步回調

當用戶成功完成支付後,微信會將相關支付信息推送到在統一下單時提交的notify_url指定的url地址中。在這一步,咱們主要要作的是檢驗信息,好比簽名是否正確、支付金額是否相同,能夠在這個過程當中修改訂單的支付狀態。
若是檢驗經過後,咱們須要給微信返回相似以下的參數:

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
</xml>

在這一步可能遇到的問題是沒法接收到微信推送過來的參數,因爲這裏公司採用的是Flask,所以採用以下的方式來進行接收:

from flask import request

...

@app.route('/notify')
def notify():
   req = request.stream.read()
   ...

在這裏,我採用的是從原始流中進行直接讀取操做。
說完了微信,咱們來看下支付寶的狀況。

支付寶

這裏我採用UTF-8編碼進行處理,並查看以下的功能流程,讓咱們對支付流程有1個瞭解。

準備

在正式開始支付寶的支付以前,咱們先來講下基礎的一些內容,首先是要使用的私鑰要是PKCS8格式的。而後是須要傳遞給支付寶的參數,其中基本參數partner_input_charsetsignsign_typeservice這些屬於基本參數,是必需要傳遞的參數。關於須要傳遞參數的內容請查閱參數

支付待簽名字符串生成

關於支付請求參數,咱們能夠查看下面的連接請求參數
在支付寶APP支付中,咱們須要請求參數中須要剔除sign_typesign這2個參數,並在簽名以前須要對字符串進行UTF-8的urlencode,即待簽名字符串若是有中文則必須未轉義形式顯示,例如:

_input_charset="utf-8"&notify_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"

在這裏,咱們對請求的參數進行了排序,而後請求的參數的數值須要添加雙引號。以後,咱們須要對上面的字符串進行簽名處理,這裏咱們假設咱們的私鑰以下:

-----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行中。
而後咱們使用以下的方式進行簽名操做:

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"&notify_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)

這樣咱們將獲得簽名:

FDW1YrI/FeX841orIDZ+rYyacSyDtWs4d+GPNpEMbWd38TpmePLagEIzAd8DDB3TlLxwyiA/IgGYIiLPQOk8qdIdp3AkjWHEMPmRbULZx2bMVNJlJy/yunOAbJRIJhP3I1Ip/nCFRVvBmBE3I8Mt95UQtYhtLkx+fZbuXmpCckQ=

在這裏,官方所說的是SHAWithRSA函數對應於PKCS1_V1_5標準外加SHA1加密方式,須要主要的是這裏生成的私鑰的長度是1024位。
而後咱們對參數字符串進行拼接將獲得:

_input_charset="utf-8"&notify_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_FINISHEDTRADE_CLOSED,WAIT_BUYER_PAY,分別對應交易成功、交易完成、交易關閉和等待買家付款。

而支付寶會傳遞過來的參數,咱們能夠查看服務器異步通知參數
在異步回調中,咱們須要完成以下2個驗證的工做:

  1. 驗證簽名

  2. 驗證是不是支付寶發來的通知

對於第2個驗證,咱們須要拼裝成以下的URL:

https://mapi.alipay.com/gateway.do?service=notify_verify&partner=2088002396712354&notify_id=RqPnCoPT3K9%252Fvwbh3I%252BFioE227%252BPfNMl8jwyZqMIiXQWxhOCmQ5MQO%252FWd93rvCB%252BaiGg

而後咱們進行GET請求,而結果會返回1個true或false的字符串。
對於第1種驗證,假設咱們有以下的字符串:

discount=0.00&payment_type=8&subject=測試&trade_no=2013082244524842&buyer_email=dlwdgl@gmail.com&gmt_create=2013-08-22 14:45:23&notify_type=trade_status_sync&quantity=1&out_trade_no=082215222612710&seller_id=2088501624816263&notify_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&notify_id=64ce1b6ab92d00ede0ee56ade98fdf2f4c&use_coupon=N&sign_type=RSA&sign=1glihU9DPWee+UJ82u3+mw3Bdnr9u01at0M/xJnPsGuHh+JA5bk3zbWaoWhU6GmLab3dIM4JNdktTcEUI9/FBGhgfLO39BKX/eBCFQ3bXAmIZn4l26fiwoO613BptT44GTEtnPiQ6+tnLsGlVSrFZaLB9FVhrGfipH2SWJcnwYs=

咱們剔除了signsign_type參數後,按照ASCII順序進行排序,咱們將獲得以下的字符串:

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&notify_time=2013-08-22 14:45:24&notify_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

而後咱們進行以下的驗證簽名:

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&notify_time=2013-08-22 14:45:24&notify_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...

相關文章
相關標籤/搜索