近期公司的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
参数,然后按照调起支付接口中组装请求参数,假设我们得到如下的请求参数: