关于Python下的支付宝APP支付

最近在折腾支付宝的移动支付(alipay.trade.app.pay), 直观的来说就是让iOS或者Android应用唤起支付宝,支付宝支付成功之后再返回应用。

支付宝APP支付没有提供Python的SDK,后台和iOS以及Android的对接难免遇到了些问题,尤其是SHA1withRSA签名的部分。本文将会简单介绍一下支付宝APP支付的一些流程以及原理,希望能够给后来的Pythoner一些解决思路。

支付宝APP支付的流程

  1. 服务端拼接Order String, 对Order String进行SHA1withRSA签名并返回给APP,注意此时服务端并没有向支付宝发起请求。
  2. APP调用支付宝的SDK,传入Order String进行支付。
  3. 支付成功之后,本地APP支付宝同步传回的result。服务端异步收到支付宝的消息。

Order String的拼接

将字典的key,value按照字母表的顺序排列在一起即可。比如原本是一个字典:

data = {
"out_trade_no": "201601020304",
"biz_content": {"product": "xxx", "title": "xxx"}
}

排序之后得到 biz_content={"product":"xxx","title":"xxx"}&out_trade_no=201601020304
。请注意这里的value并没有进行url encode。

以下的代码可以帮你生成Order String。biz_content是一个json的字符串,因此biz_content里面的内容理论上是不需要排序的。

import json
def ordered_data(data):
complex_keys = []
for key, value in data.items():
if isinstance(value, dict):
complex_keys.append(key)

# 将字典类型的数据单独排序
for key in complex_keys:
data[key] = json.dumps(data[key], sort_keys=True).replace(" ", "")

return sorted([(k, v) for k, v in data.items()])

unsigned_items = ordered_data(data)
unsigned_string = "&".join("{}={}".format(k, v) for k, v in unsigned_items)

签名

RSA私钥的生成

支付宝官方文档
里面给出了生成私钥的方法, openssl貌似更加方便一点:

OpenSSL> genrsa -out app_private_key.pem   1024  #生成私钥
OpenSSL> pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem #Java开发者需要将私钥转换成PKCS8格式
OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem #生成公钥
OpenSSL> exit #退出OpenSSL程序

运行上述的命令, 我们会得到三个文件:

  • app_private_key.pem --这是 pkcs#1格式的私钥
  • app_private_key_pkcs8.pem -- 这是pkcs#8格式的私钥
  • app_public_key.pem -- 这是pkcs#8格式的公钥

我们下面的Python和openssl用到的private key file,同时支持PKCS#1 和PKCS#8两种格式,因此可以传入任意一种格式的Private Key。

openssl对字符串进行SHA1withRSA签名

支付宝的规则是对Order String 进行RSAwithSHA1 签名,也即对Order String先SHA1得到摘要,然后对这个摘要进行签名。在继续阅读本文前,你需要明白摘要以及签名是什么意思。

运行以下命令可以对 abcn
(注意echo会额外传入一 个n
)进行签名, 然后输出base64编码的结果。

echo "abc" | openssl sha1 -sign privatekey.file | openssl base64

SHA1withRSA的Python实现

运行以下命令可以使用Python对 abcn
进行签名:

from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
from Crypto.PublicKey import RSA

def sign_string(private_key_path, unsigned_string):
# 开始计算签名
key = RSA.importKey(open(private_key_path).read())
signer = PKCS1_v1_5.new(key)
signature = signer.sign(SHA.new(unsigned_string.encode("utf8")))
# base64 编码,转换为unicode表示并移除回车
sign = base64.encodebytes(signature).decode("utf8").replace("n", "")
return sign

signed_string = sign_string(private_key_path, "abcn")

Crypto
是Python自带的库。通过比较openssl以及Python计算 abcn
的结果,如果二者相同则证明Python的签名实现正确。之后你就可以对更加复杂的Order String 进行签名了。

签名验证

我们之前已经获得了对 abcn
进行签名的base64结果。现在可以用公钥验证之前生成的结构是否正确,以下的代码, 如果你的result为True那么表明验证签名功能是正确的。

def validate_sign(public_key_path, message, signature):
# 开始计算签名
key = RSA.importKey(open(public_key_path).read())
signer = PKCS1_v1_5.new(key)
digest = SHA.new()
digest.update(message.encode("utf8"))
if signer.verify(digest, base64.decodestring(signature.encode("utf8"))):
return True
return False

result = validate_sign(your_public_key_path, "abcn", signed_string)

再次生成 Order String

首先,我们之前已经生成过一次Order String: biz_content={"product":"xxx","title":"xxx"}&out_trade_no=201601020304
。生成的签名也需要被添加到里面: biz_content={"product":"xxx","title":"xxx"}&out_trade_no=201601020304&sign=OMXv7kLz

其次,服务器返回给APP的Order String是需要URL Encode的。也就是将key=value转换成key=quote_plus(value)。也就是这样的: biz_content=%7B%22product%22%3A%22xxx%22%2C%22title%22%3A%22xxx%22%7D&out_trade_no=201601020304&sign=OMXv7kLz
.

异步回调支付结果的验证

之前我们有使用过自己的公钥验证自己的私钥签名的结果。验证支付结果回调请求的原理类似,只是需要使用支付宝的公钥。

其他

支付宝的消息返回非常不友好,比如唤起支付宝支付的时候,你有时候会看到错误 ALI40247

出现这种错误的原因有很多,签名错误是常见的一种。此时你需要使用openssl原生命令计算SHA1withRSA,再对比自己的结果,这样就可以保证签名是对的(再次说明,echo会传入额外的 n
给openssl,你最好从文件读入需要openssl加密的字符串)。

当然也有其他原因,比如我传入参数 appid
而实际上应该传入 app_id
。当然坑爹的支付宝是不会有这么详细的提示的。

  • 版权声明: 本文源自互联网, 于2个月前,由整理发表,共 3472字。
  • 原文链接:点此查看原文