玩转APP支付

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

近期公司的APP打算上线,需要集成支付的功能。由于采用的是Python进行开发,因此无法直接使用官方提供的SDK。虽然也有一些集成的第3方可以使用,比如ping++beecloud
但是由于提供的时间比较充裕,于是就自己实现了1个。在这个过程中,难免遇到一些坑,而这些坑有时会困扰你很久。
最初,并没有打算写这么一篇文章,因为它的适用范围很窄。但是网上搜索到的关于APP支付方面的都是移动端iOS和Android的实现方式,对于服务端的实现寥寥无几。相比而言,python在当前毕竟是小众语言,而如果参考其他语言,比如php的实现,发现这个过程还是有不少地方是没有讲清楚的。
虽然对于很多开发者来说,支付这个功能涉及的知识点并不是很多,但是你会发现你却在这里耗费了很多的时间。有时1个签名的问题,就让你无法调用支付,比如支付宝的Alipay10问题,总是出现服务器繁忙的提示,其实就是你的签名出了问题。
在这里,由于涉及到公司的一些敏感信息的问题,因此下面代码中的签名用的都是测试数据,而签名是根据已经验证通过的函数调用计算出来的。当你发现自己签名不过时,可以直接复制这些字符串,然后比对下面计算出来的签名来查看你的签名函数及你的回调处理哪里出了问题。

适用范围

首先为了避免耽误大家的时间,这里我们只实现了微信支付及支付宝的移动支付。对于微信公众支付及支付宝的其他支付场景是不适用的。
这里,限于篇幅,只对订单支付及异步回调的部分进行说明,因为如果把所有的接口都过一遍,太耗费时间,还不如直接在pypi上上传1个包,直接使用pip安装。
在这里,将用到的签名的方式单独提取出来进行讲解,对于相同产品其他的接口也是适用的,只是请求的参数有所变化而已。

个人建议及使用的库

在正式讲述APP支付之前,我有如下的建议:

  1. Python版本>=2.7.9,由于Python版本2.7.9为1个bug修复版本,在这个版本中使用新的SSL模块,修复了之前HTTP客户端模块(比如urllib2,httplib)不对服务器证书进行校验的问题,详情请查看PEP 476

  2. 使用lxml,而不是标准库中的XML库,主要在于标准库中的XML模块无法检验恶意构造的数据,详情请查看Warning

  3. 使用pycrypto库用于支付宝RSA签名,版本>=2.61。这里使用的是pycrypto,是因为安装比较方便,另外因为版本2.61之前在某种情况下,使用fork会出现随机数不安全的问题,详情请查看CVE-2013-1445

职责

下面我们需要理清我们要做的事情,避免不必要的工作。主要是如下2个方面:

  • 服务端负责生成订单及签名,及接受支付异步通知

  • 客户端负责使用服务端传来的订单信息调用支付接口,及根据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…

    原文作者:我勒个去
    原文地址: https://segmentfault.com/a/1190000006053618
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞