目前國內比較流行的第三方支付主要有支付寶和微信支付,博主最近研究了下如何用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")
測試支付結果: