使用Tornado异步接入第三方(支付宝)支付
目前国内比较流行的第三方支付主要有支付宝和微信支付,博主最近研究了下如何用Python接入支付宝支付,这里我以Tornado作为web框架,接入支付宝构造支付接口。
使用Tornado异步接入支付宝支付流程:
1. 进入蚂蚁金服开放平台填写开发者信息、应用信息
2. 配置RSA256密钥,生成支付宝和应用的密钥
3. 构造订单接口API,生成订单
4. 构造支付接口
1. 进入蚂蚁金服开放平台填写开发者信息、应用信息
这里通过沙箱环境开发测试接口,蚂蚁金服开放平台–>开发者中心–>研发者服务–>沙箱应用,配置沙箱应用信息:
设置授权回调地址,注意:这个地址一定要是外网IP地址(我这里是我的阿里云服务器地址),回调地址是自己支付完回调的api地址,可通过扫码下载沙箱板支付宝钱包进行支付测试:
设置沙箱账号,设置买家和买家的测试账号,支付宝会默认给买家账户99999元,可用来测试支付接口是否成功:
2. 配置RSA256密钥,生成支付宝和应用的密钥
支付宝默认有两种加密算法生成密钥:RSA(SHA1)和RSA2(SHA256),鉴于安全性支付宝推荐使用RSA2(SHA256)密钥。通过查看密钥生成文档https://docs.open.alipay.com/291/105971得知密钥生成方法,按文档提示下载密钥生成工具,解压后打开生成工具,选择密码格式(Python当然就是选择PKCS1了)和密码长度,生成公钥和私钥:
生成后可在RSA密钥文件夹下查看应用的公钥和私钥,并将应用公钥上传到开放平台的开发者环境中:
3. 构造订单接口API,生成订单
查看支付接口文档:https://docs.open.alipay.com/270/alipay.trade.page.pay/可知:
支付接口的必填参数有out_trade_no(订单号)、total_amount(订单金额)、subject(订单标题),所以先构造订单接口,生成订单:
1 class OrderSnHandler(BaseHandler):
2 @authenticated
3 async def post(self, *args, **kwargs):
4 """
5 创建订单信息
6 :param request:
7 :return:
8 """
9 res_data = {}
10 req_data = self.request.body.decode("utf8")
11 req_data = json.loads(req_data)
12 post_script = req_data.get("post_script")
13 order_form = TradeOrderSnForm.from_json(req_data)
14 if order_form.validate():
15 try:
16 order_mount = order_form.order_mount.data
17 orders_object = await self.application.objects.create(
18 OrderInfo,
19 pay_status=OrderInfo.ORDER_STATUS[4][0],
20 pay_time=datetime.now(),
21 order_sn=OrderInfo.generate_order_sn(),
22 user=self.current_user,
23 order_mount=order_mount,
24 post_script=post_script
25 )
26 res_data["id"] = orders_object.id
27 except Exception:
28 self.set_status(400)
29 res_data["content"] = "订单创建失败"
30 else:
31 res_data["content"] = order_form.errors
32
33 self.finish(res_data)
4. 构造支付接口
(1) 构造支付接口类
流程:RSA导入公钥和私钥–>构造请求参数biz_content–>构造支付宝公共请求参数–>排序并拼接参数为规范字符串–>生成签名后的字符串–>请求支付宝接口–>对支付宝接口返回的数据进行签名比对
1 class AliPay(object):
2 """
3 支付宝支付接口
4 """
5
6 def __init__(self, appid, app_notify_url, app_private_key_path,
7 alipay_public_key_path, return_url, debug=False):
8 self.appid = appid
9 self.app_notify_url = app_notify_url
10 self.app_private_key_path = app_private_key_path
11 self.app_private_key = None
12 self.return_url = return_url
13 with open(self.app_private_key_path) as fp:
14 self.app_private_key = RSA.importKey(fp.read())
15
16 self.alipay_public_key_path = alipay_public_key_path
17 with open(self.alipay_public_key_path) as fp:
18 self.alipay_public_key = RSA.import_key(fp.read())
19
20 if debug is True:
21 self.__gateway = "https://openapi.alipaydev.com/gateway.do"
22 else:
23 self.__gateway = "https://openapi.alipay.com/gateway.do"
24
25 def direct_pay(self, subject, out_trade_no, total_amount, **kwargs): # NOQA
26 """
27 构造请求参数biz_content,
28 并将其放入公共请求参数中,
29 返回签名sign的data
30 :param subject:
31 :param out_trade_no:
32 :param total_amount:
33 :param kwargs:
34 :return:
35 """
36 biz_content = {
37 "subject": subject,
38 "out_trade_no": out_trade_no,
39 "total_amount": total_amount,
40 "product_code": "FAST_INSTANT_TRADE_PAY",
41 }
42
43 biz_content.update(kwargs)
44 data = self.build_body(
45 "alipay.trade.page.pay",
46 biz_content,
47 self.return_url
48 )
49 return self.sign_data(data)
50
51 def build_body(self, method, biz_content, return_url=None):
52 """
53 构造公共请求参数
54 :param method:
55 :param biz_content:
56 :param return_url:
57 :return:
58 """
59 data = {
60 "app_id": self.appid,
61 "method": method,
62 "charset": "utf-8",
63 "sign_type": "RSA2",
64 "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
65 "version": "1.0",
66 "biz_content": biz_content
67 }
68
69 if return_url:
70 data["notify_url"] = self.app_notify_url
71 data["return_url"] = self.return_url
72
73 return data
74
75 def sign_data(self, data):
76 """
77 拼接排序后的data,以&连接成符合规范的字符串,并对字符串签名,
78 将签名后的字符串通过quote_plus格式化,
79 将请求参数中的url格式化为safe的,获得最终的订单信息字符串
80 :param data:
81 :return:
82 """
83 # 签名中不能有sign字段
84 if "sign" in data:
85 data.pop("sign")
86
87 unsigned_items = self.ordered_data(data)
88 unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)
89 sign = self.sign_string(unsigned_string.encode("utf-8"))
90 quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)
91
92 signed_string = quoted_string + "&sign=" + quote_plus(sign)
93 return signed_string
94
95 def ordered_data(self, data):
96 """
97 将请求参数字典排序,
98 支付宝接口要求是拼接的有序参数字符串
99 :param data:
100 :return:
101 """
102 complex_keys = []
103 for key, value in data.items():
104 if isinstance(value, dict):
105 complex_keys.append(key)
106
107 for key in complex_keys:
108 data[key] = json.dumps(data[key], separators=(\',\', \':\'))
109
110 return sorted([(k, v) for k, v in data.items()])
111
112 def sign_string(self, unsigned_string):
113 """
114 生成签名,并进行base64 编码,
115 转换为unicode表示并去掉换行符
116 :param unsigned_string:
117 :return:
118 """
119 key = self.app_private_key
120 signer = PKCS1_v1_5.new(key)
121 signature = signer.sign(SHA256.new(unsigned_string))
122 sign = encodebytes(signature).decode("utf8").replace("\n", "")
123 return sign
124
125 def _verify(self, raw_content, signature):
126 """
127 对支付宝接口返回的数据进行签名比对,
128 验证是否来源于支付宝
129 :param raw_content:
130 :param signature:
131 :return:
132 """
133 key = self.alipay_public_key
134 signer = PKCS1_v1_5.new(key)
135 digest = SHA256.new()
136 digest.update(raw_content.encode("utf8"))
137 if signer.verify(digest, decodebytes(signature.encode("utf8"))):
138 return True
139 return False
140
141 def verify(self, data, signature):
142 """
143 验证支付宝返回的数据,防止是伪造信息
144 :param data:
145 :param signature:
146 :return:
147 """
148 if "sign_type" in data:
149 data.pop("sign_type")
150 unsigned_items = self.ordered_data(data)
151 message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
152 return self._verify(message, signature)
(2) 构造支付链接接口
通过步骤3创建的订单信息生成支付链接,这里接口我采用协程+异步的方式,authenticated是自定义的JWT验证装饰器,private_key_path和ali_pub_key_path是前面生成的应用私钥和支付宝公钥文件地址
1 class GenPayLinkHandler(BaseHandler):
2 @authenticated
3 async def get(self, *args, **kwargs):
4 """
5 通过订单生成支付链接
6 :param args:
7 :param kwargs:
8 :return:
9 """
10 res_data = {}
11 order_id = get_int_or_none(self.get_argument("id", None))
12 if not order_id:
13 self.set_status(400)
14 self.write({"content": "缺少order_id参数"})
15
16 try:
17 order_obj = await self.application.objects.get(
18 OrderInfo, id=order_id,
19 pay_status=OrderInfo.ORDER_STATUS[4][0]
20 )
21 out_trade_no = order_obj.order_sn
22 order_mount = order_obj.order_mount
23 subject = order_obj.post_script
24 alipay = AliPay(
25 appid=settings["ALI_APPID"],
26 app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]),
27 app_private_key_path=settings["private_key_path"],
28 alipay_public_key_path=settings["ali_pub_key_path"],
29 debug=True,
30 return_url="{}/alipay/return/".format(settings["SITE_URL"])
31 )
32 url = alipay.direct_pay(
33 subject=subject,
34 out_trade_no=out_trade_no,
35 total_amount=order_mount,
36 return_url="{}/alipay/return/".format(settings["SITE_URL"])
37 )
38 re_url = settings["RETURN_URI"].format(data=url)
39 res_data["re_url"] = re_url
40 except OrderInfo.DoesNotExist:
41 self.set_status(400)
42 res_data["content"] = "订单不存在"
43
44 self.finish(res_data)
返回结果:
打开支付链接可以看到:
(3) 构造支付的回调接口
在支付完成后,支付宝会调用在开发者信息中配置的回调url,通过GET方法回调return_ul,通过POST方法发送notify主动通知商户返回服务器里指定的页面,这里分别实现return_ul和notify_url对应的接口,支付宝返回的notify_url是个异步的所以我这里也以异步的方式实现这个接口:
1 class AlipayHandler(BaseHandler):
2 def get(self, *args, **kwargs):
3 """
4 处理支付宝的return_url返回
5 :param request:
6 :return:
7 """
8 res_data = {}
9 processed_dict = {}
10 req_data = self.request.arguments
11 req_data = format_arguments(req_data)
12 for key, value in req_data.items():
13 processed_dict[key] = value[0]
14
15 sign = processed_dict.pop("sign", None)
16 alipay = AliPay(
17 appid=settings["ALI_APPID"],
18 app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]),
19 app_private_key_path=settings["private_key_path"],
20 alipay_public_key_path=settings["ali_pub_key_path"],
21 debug=True,
22 return_url="{}/alipay/return/".format(settings["SITE_URL"])
23 )
24
25 verify_re = alipay.verify(processed_dict, sign)
26
27 if verify_re is True:
28 res_data["content"] = "success"
29 else:
30 res_data["content"] = "Failed"
31
32 self.finish(res_data)
33
34 async def post(self, *args, **kwargs):
35 """
36 处理支付宝的notify_url
37 :param request:
38 :return:
39 """
40 processed_dict = {}
41 req_data = self.request.body_arguments
42 req_data = format_arguments(req_data)
43 for key, value in req_data.items():
44 processed_dict[key] = value[0]
45
46 sign = processed_dict.pop("sign", None)
47 alipay = AliPay(
48 appid=settings["ALI_APPID"],
49 app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]),
50 app_private_key_path=settings["private_key_path"],
51 alipay_public_key_path=settings["ali_pub_key_path"],
52 debug=True,
53 return_url="{}/alipay/return/".format(settings["SITE_URL"])
54 )
55
56 verify_re = alipay.verify(processed_dict, sign)
57
58 if verify_re is True:
59 order_sn = processed_dict.get(\'out_trade_no\')
60 trade_no = processed_dict.get(\'trade_no\')
61 trade_status = processed_dict.get(\'trade_status\')
62
63 orders_query = OrderInfo.update(
64 pay_status=trade_status,
65 trade_no=trade_no,
66 pay_time=datetime.now()
67 ).where(
68 OrderInfo.order_sn == order_sn
69 )
70 await self.application.objects.execute(
71 orders_query
72 )
73
74 self.finish("success")
测试支付结果: