使用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 3async def post(self, *args, **kwargs): 4""" 5建立訂單資訊 6:param request: 7:return: 8""" 9res_data = {} 10req_data = self.request.body.decode("utf8") 11req_data = json.loads(req_data) 12post_script = req_data.get("post_script") 13order_form = TradeOrderSnForm.from_json(req_data) 14if order_form.validate(): 15try: 16order_mount = order_form.order_mount.data 17orders_object = await self.application.objects.create( 18OrderInfo, 19pay_status=OrderInfo.ORDER_STATUS[4][0], 20pay_time=datetime.now(), 21order_sn=OrderInfo.generate_order_sn(), 22user=self.current_user, 23order_mount=order_mount, 24post_script=post_script 25) 26res_data["id"] = orders_object.id 27except Exception: 28self.set_status(400) 29res_data["content"] = "訂單建立失敗" 30else: 31res_data["content"] = order_form.errors 32 33self.finish(res_data)
4. 構造支付介面
(1) 構造支付介面類
流程:RSA匯入公鑰和私鑰--> 構造請求引數biz_content-->構造支付寶公共請求引數--> 排序並拼接引數為規範字串-->生成簽名後的字串-->請求支付寶介面--> 對支付寶介面返回的資料進行簽名比對
1 class AliPay(object): 2""" 3支付寶支付介面 4""" 5 6def __init__(self, appid, app_notify_url, app_private_key_path, 7alipay_public_key_path, return_url, debug=False): 8self.appid = appid 9self.app_notify_url = app_notify_url 10self.app_private_key_path = app_private_key_path 11self.app_private_key = None 12self.return_url = return_url 13with open(self.app_private_key_path) as fp: 14self.app_private_key = RSA.importKey(fp.read()) 15 16self.alipay_public_key_path = alipay_public_key_path 17with open(self.alipay_public_key_path) as fp: 18self.alipay_public_key = RSA.import_key(fp.read()) 19 20if debug is True: 21self.__gateway = "https://openapi.alipaydev.com/gateway.do" 22else: 23self.__gateway = "https://openapi.alipay.com/gateway.do" 24 25def 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""" 36biz_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 43biz_content.update(kwargs) 44data = self.build_body( 45"alipay.trade.page.pay", 46biz_content, 47self.return_url 48) 49return self.sign_data(data) 50 51def 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""" 59data = { 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 69if return_url: 70data["notify_url"] = self.app_notify_url 71data["return_url"] = self.return_url 72 73return data 74 75def sign_data(self, data): 76""" 77拼接排序後的data,以&連線成符合規範的字串,並對字串簽名, 78將簽名後的字串通過quote_plus格式化, 79將請求引數中的url格式化為safe的,獲得最終的訂單資訊字串 80:param data: 81:return: 82""" 83# 簽名中不能有sign欄位 84if "sign" in data: 85data.pop("sign") 86 87unsigned_items = self.ordered_data(data) 88unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items) 89sign = self.sign_string(unsigned_string.encode("utf-8")) 90quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items) 91 92signed_string = quoted_string + "&sign=" + quote_plus(sign) 93return signed_string 94 95def ordered_data(self, data): 96""" 97將請求引數字典排序, 98支付寶介面要求是拼接的有序引數字串 99:param data: 100:return: 101""" 102complex_keys = [] 103for key, value in data.items(): 104if isinstance(value, dict): 105complex_keys.append(key) 106 107for key in complex_keys: 108data[key] = json.dumps(data[key], separators=(',', ':')) 109 110return sorted([(k, v) for k, v in data.items()]) 111 112def sign_string(self, unsigned_string): 113""" 114生成簽名,並進行base64 編碼, 115轉換為unicode表示並去掉換行符 116:param unsigned_string: 117:return: 118""" 119key = self.app_private_key 120signer = PKCS1_v1_5.new(key) 121signature = signer.sign(SHA256.new(unsigned_string)) 122sign = encodebytes(signature).decode("utf8").replace("\n", "") 123return sign 124 125def _verify(self, raw_content, signature): 126""" 127對支付寶介面返回的資料進行簽名比對, 128驗證是否來源於支付寶 129:param raw_content: 130:param signature: 131:return: 132""" 133key = self.alipay_public_key 134signer = PKCS1_v1_5.new(key) 135digest = SHA256.new() 136digest.update(raw_content.encode("utf8")) 137if signer.verify(digest, decodebytes(signature.encode("utf8"))): 138return True 139return False 140 141def verify(self, data, signature): 142""" 143驗證支付寶返回的資料,防止是偽造資訊 144:param data: 145:param signature: 146:return: 147""" 148if "sign_type" in data: 149data.pop("sign_type") 150unsigned_items = self.ordered_data(data) 151message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items) 152return self._verify(message, signature)
(2) 構造支付連結介面
通過步驟3建立的訂單資訊生成支付連結,這裡介面我採用協程+非同步的方式, authenticated是自定義的JWT驗證裝飾器, private_key_path和 ali_pub_key_path是前面生成的應用私鑰和支付寶公鑰檔案地址
1 class GenPayLinkHandler(BaseHandler): 2@authenticated 3async def get(self, *args, **kwargs): 4""" 5通過訂單生成支付連結 6:param args: 7:param kwargs: 8:return: 9""" 10res_data = {} 11order_id = get_int_or_none(self.get_argument("id", None)) 12if not order_id: 13self.set_status(400) 14self.write({"content": "缺少order_id引數"}) 15 16try: 17order_obj = await self.application.objects.get( 18OrderInfo, id=order_id, 19pay_status=OrderInfo.ORDER_STATUS[4][0] 20) 21out_trade_no = order_obj.order_sn 22order_mount = order_obj.order_mount 23subject = order_obj.post_script 24alipay = AliPay( 25appid=settings["ALI_APPID"], 26app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]), 27app_private_key_path=settings["private_key_path"], 28alipay_public_key_path=settings["ali_pub_key_path"], 29debug=True, 30return_url="{}/alipay/return/".format(settings["SITE_URL"]) 31) 32url = alipay.direct_pay( 33subject=subject, 34out_trade_no=out_trade_no, 35total_amount=order_mount, 36return_url="{}/alipay/return/".format(settings["SITE_URL"]) 37) 38re_url = settings["RETURN_URI"].format(data=url) 39res_data["re_url"] = re_url 40except OrderInfo.DoesNotExist: 41self.set_status(400) 42res_data["content"] = "訂單不存在" 43 44self.finish(res_data)
返回結果:
開啟支付連結可以看到:
(3) 構造支付的回撥介面
在支付完成後,支付寶會呼叫在開發者資訊中配置的回撥url,通過GET方法回撥return_ul,通過POST方法傳送notify主動通知商戶返回伺服器裡指定的頁面,這裡分別實現return_ul和notify_url對應的介面,支付寶返回的notify_url是個非同步的所以我這裡也以非同步的方式實現這個介面:
1 class AlipayHandler(BaseHandler): 2def get(self, *args, **kwargs): 3""" 4處理支付寶的return_url返回 5:param request: 6:return: 7""" 8res_data = {} 9processed_dict = {} 10req_data = self.request.arguments 11req_data = format_arguments(req_data) 12for key, value in req_data.items(): 13processed_dict[key] = value[0] 14 15sign = processed_dict.pop("sign", None) 16alipay = AliPay( 17appid=settings["ALI_APPID"], 18app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]), 19app_private_key_path=settings["private_key_path"], 20alipay_public_key_path=settings["ali_pub_key_path"], 21debug=True, 22return_url="{}/alipay/return/".format(settings["SITE_URL"]) 23) 24 25verify_re = alipay.verify(processed_dict, sign) 26 27if verify_re is True: 28res_data["content"] = "success" 29else: 30res_data["content"] = "Failed" 31 32self.finish(res_data) 33 34async def post(self, *args, **kwargs): 35""" 36處理支付寶的notify_url 37:param request: 38:return: 39""" 40processed_dict = {} 41req_data = self.request.body_arguments 42req_data = format_arguments(req_data) 43for key, value in req_data.items(): 44processed_dict[key] = value[0] 45 46sign = processed_dict.pop("sign", None) 47alipay = AliPay( 48appid=settings["ALI_APPID"], 49app_notify_url="{}/alipay/return/".format(settings["SITE_URL"]), 50app_private_key_path=settings["private_key_path"], 51alipay_public_key_path=settings["ali_pub_key_path"], 52debug=True, 53return_url="{}/alipay/return/".format(settings["SITE_URL"]) 54) 55 56verify_re = alipay.verify(processed_dict, sign) 57 58if verify_re is True: 59order_sn = processed_dict.get('out_trade_no') 60trade_no = processed_dict.get('trade_no') 61trade_status = processed_dict.get('trade_status') 62 63orders_query = OrderInfo.update( 64pay_status=trade_status, 65trade_no=trade_no, 66pay_time=datetime.now() 67).where( 68OrderInfo.order_sn == order_sn 69) 70await self.application.objects.execute( 71orders_query 72) 73 74self.finish("success")
測試支付結果: