1. 程式人生 > >使用Tornado非同步接入第三方(支付寶)支付

使用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")

測試支付結果: