玩转APP支付

适用范围

首先为了避免耽误大家的时间,这里我们只实现了微信支付及支付宝的移动支付。对于微信公众支付及支付宝的其他支付场景是不适用的。
这里,限于篇幅,只对订单支付及异步回调的部分进行说明,因为如果把所有的接口都过一遍,太耗费时间,还不如直接在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端调起支付接口中的其他字段。

统一下单

这里,假设我们统一下单时请求参数如下:

1
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,那么我们需要将商户号与之前的请求参数拼接在一起:

1
2
3
4
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加密后将其转换为大写字母,这样就得到我们需要的签名了。因此,在请求统一下单时,我们需要传递如下的字符串:

Python

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&timestamp=1412000000

那么进行签名后将得到字符串0586C6E4A2AA6D297F4046362D878BAC。那么我们返回给客户端APP的字段主要有prepayidnoncestrtimestampsign

异步回调

当用户成功完成支付后,微信会将相关支付信息推送到在统一下单时提交的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_charsetsignsign_typeservice这些属于基本参数,是必须要传递的参数。关于需要传递参数的内容请查阅参数

支付待签名字符串生成

关于支付请求参数,我们可以查看下面的链接请求参数
在支付宝APP支付中,我们需要请求参数中需要剔除sign_typesign这2个参数,并在签名之前需要对字符串进行UTF-8的urlencode,即待签名字符串如果有中文则必须未转义形式显示,例如:

1
_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”

在这里,我们对请求的参数进行了排序,然后请求的参数的数值需要添加双引号。之后,我们需要对上面的字符串进行签名处理,这里我们假设我们的私钥如下:

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″&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)

这样我们将得到签名:

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

在这里,官方所说的是SHAWithRSA函数对应于PKCS1_V1_5标准外加SHA1加密方式,需要主要的是这里生成的私钥的长度是1024位。
然后我们对参数字符串进行拼接将得到:

1
_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:

1
https://mapi.alipay.com/gateway.do?service=notify_verify&partner=2088002396712354&notify_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=20130822 14:45:23&notify_type=trade_status_sync&quantity=1&out_trade_no=082215222612710&seller_id=2088501624816263&notify_time=20130822 14:45:24&body=测试测试&trade_status=TRADE_SUCCESS&is_total_fee_adjust=N&total_fee=1.00&gmt_payment=20130822 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顺序进行排序,我们将得到如下的字符串:

1
body=测试测试&buyer_email=dlwdgl@gmail.com&buyer_id=2088602315385429&discount=0.00&gmt_create=20130822 14:45:23&gmt_payment=20130822 14:45:24&is_total_fee_adjust=N&notify_time=20130822 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

然后我们进行如下的验证签名:

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&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…

转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn

8 Comments