#!/usr/bin/env python # coding: utf-8 """ __init__.py ~~~~~~~~~~ """ import json from datetime import datetime from functools import partial import hashlib import OpenSSL from Cryptodome.Hash import SHA, SHA256 from Cryptodome.PublicKey import RSA from Cryptodome.Signature import PKCS1_v1_5 from .compat import decodebytes, encodebytes, quote_plus, urlopen from .exceptions import AliPayException, AliPayValidationError from .utils import AliPayConfig from .loggers import logger # 常见加密算法 CryptoAlgSet = ( b'rsaEncryption', b'md2WithRSAEncryption', b'md5WithRSAEncryption', b'sha1WithRSAEncryption', b'sha256WithRSAEncryption', b'sha384WithRSAEncryption', b'sha512WithRSAEncryption' ) class BaseAliPay: @property def appid(self): return self._appid @property def sign_type(self): return self._sign_type @property def app_private_key(self): """签名用""" return self._app_private_key @property def alipay_public_key(self): """验证签名用""" return self._alipay_public_key def __init__( self, appid, app_notify_url, app_private_key_string=None, alipay_public_key_string=None, sign_type="RSA2", debug=False, verbose=False, config=None ): """ 初始化: alipay = AliPay( appid="", app_notify_url="http://example.com", sign_type="RSA2" ) """ self._appid = str(appid) self._app_notify_url = app_notify_url self._app_private_key_string = app_private_key_string self._alipay_public_key_string = alipay_public_key_string self._verbose = verbose self._config = config or AliPayConfig() self._app_private_key = None self._alipay_public_key = None if sign_type not in ("RSA", "RSA2"): message = "Unsupported sign type {}".format(sign_type) raise AliPayException(None, message) self._sign_type = sign_type if debug: self._gateway = "https://openapi.alipaydev.com/gateway.do" else: self._gateway = "https://openapi.alipay.com/gateway.do" # load key file immediately self._load_key() def _load_key(self): # load private key content = self._app_private_key_string self._app_private_key = RSA.importKey(content) # load public key content = self._alipay_public_key_string self._alipay_public_key = RSA.importKey(content) def _sign(self, unsigned_string): """ 通过如下方法调试签名 方法1 key = rsa.PrivateKey.load_pkcs1(open(self._app_private_key_string).read()) sign = rsa.sign(unsigned_string.encode(), key, "SHA-1") # base64 编码,转换为unicode表示并移除回车 sign = base64.encodebytes(sign).decode().replace("\n", "") 方法2 key = RSA.importKey(open(self._app_private_key_string).read()) signer = PKCS1_v1_5.new(key) signature = signer.sign(SHA.new(unsigned_string.encode())) # base64 编码,转换为unicode表示并移除回车 sign = base64.encodebytes(signature).decode().replace("\n", "") 方法3 echo "abc" | openssl sha1 -sign alipay.key | openssl base64 """ # 开始计算签名 key = self.app_private_key signer = PKCS1_v1_5.new(key) if self._sign_type == "RSA": signature = signer.sign(SHA.new(unsigned_string.encode())) else: signature = signer.sign(SHA256.new(unsigned_string.encode())) # base64 编码,转换为unicode表示并移除回车 sign = encodebytes(signature).decode().replace("\n", "") return sign def _ordered_data(self, data): for k, v in data.items(): if isinstance(v, dict): # 将字典类型的数据dump出来 data[k] = json.dumps(v, separators=(',', ':')) return sorted(data.items()) def build_body(self, method, biz_content, **kwargs): data = { "app_id": self._appid, "method": method, "charset": "utf-8", "sign_type": self._sign_type, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "biz_content": biz_content } data.update(kwargs) if method in ( "alipay.trade.app.pay", "alipay.trade.wap.pay", "alipay.trade.page.pay", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.create" ) and not data.get("notify_url") and self._app_notify_url: data["notify_url"] = self._app_notify_url if self._verbose: logger.debug("data to be signed") logger.debug(data) return data def sign_data(self, data): # 排序后的字符串 ordered_items = self._ordered_data(data) raw_string = "&".join("{}={}".format(k, v) for k, v in ordered_items) sign = self._sign(raw_string) unquoted_items = ordered_items + [('sign', sign)] # 获得最终的订单信息字符串 signed_string = "&".join("{}={}".format(k, quote_plus(v)) for k, v in unquoted_items) if self._verbose: logger.debug("signed srtring") logger.debug(signed_string) return signed_string def _verify(self, raw_content, signature): # 开始计算签名 key = self.alipay_public_key signer = PKCS1_v1_5.new(key) if self._sign_type == "RSA": digest = SHA.new() else: digest = SHA256.new() digest.update(raw_content.encode()) return bool(signer.verify(digest, decodebytes(signature.encode()))) def verify(self, data, signature): if "sign_type" in data: sign_type = data.pop("sign_type") if sign_type != self._sign_type: raise AliPayException(None, "Unknown sign type: {}".format(sign_type)) # 排序后的字符串 unsigned_items = self._ordered_data(data) message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items) return self._verify(message, signature) def client_api(self, api_name, biz_content, **kwargs): """ alipay api without http request """ data = self.build_body(api_name, biz_content, **kwargs) return self.sign_data(data) def server_api(self, api_name, biz_content, **kwargs): """ alipay api with http request """ data = self.build_body(api_name, biz_content, **kwargs) # alipay.trade.query => alipay_trade_query_response response_type = api_name.replace(".", "_") + "_response" print(data) return self.verified_sync_response(data, response_type) def api_alipay_trade_wap_pay( self, subject, out_trade_no, total_amount, return_url=None, notify_url=None, **kwargs ): biz_content = { "subject": subject, "out_trade_no": out_trade_no, "total_amount": total_amount, "product_code": "QUICK_WAP_PAY" } biz_content.update(kwargs) data = self.build_body( "alipay.trade.wap.pay", biz_content, return_url=return_url, notify_url=notify_url ) return self.sign_data(data) def api_alipay_trade_app_pay( self, subject, out_trade_no, total_amount, notify_url=None, **kwargs ): biz_content = { "subject": subject, "out_trade_no": out_trade_no, "total_amount": total_amount, "product_code": "QUICK_MSECURITY_PAY" } biz_content.update(kwargs) data = self.build_body("alipay.trade.app.pay", biz_content, notify_url=notify_url) return self.sign_data(data) def api_alipay_trade_page_pay(self, subject, out_trade_no, total_amount, return_url=None, notify_url=None, **kwargs): biz_content = { "subject": subject, "out_trade_no": out_trade_no, "total_amount": total_amount, "product_code": "FAST_INSTANT_TRADE_PAY" } biz_content.update(kwargs) data = self.build_body( "alipay.trade.page.pay", biz_content, return_url=return_url, notify_url=notify_url ) return self.sign_data(data) def api_alipay_trade_query(self, out_trade_no=None, trade_no=None): """ response = { "alipay_trade_query_response": { "trade_no": "2017032121001004070200176844", "code": "10000", "invoice_amount": "20.00", "open_id": "20880072506750308812798160715407", "fund_bill_list": [ { "amount": "20.00", "fund_channel": "ALIPAYACCOUNT" } ], "buyer_logon_id": "csq***@sandbox.com", "send_pay_date": "2017-03-21 13:29:17", "receipt_amount": "20.00", "out_trade_no": "out_trade_no15", "buyer_pay_amount": "20.00", "buyer_user_id": "2088102169481075", "msg": "Success", "point_amount": "0.00", "trade_status": "TRADE_SUCCESS", "total_amount": "20.00" }, "sign": "" } """ assert (out_trade_no is not None) or (trade_no is not None), \ "Both trade_no and out_trade_no are None" biz_content = {} if out_trade_no: biz_content["out_trade_no"] = out_trade_no if trade_no: biz_content["trade_no"] = trade_no data = self.build_body("alipay.trade.query", biz_content) response_type = "alipay_trade_query_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_pay( self, out_trade_no, scene, auth_code, subject, notify_url=None, **kwargs ): """ eg: self.api_alipay_trade_pay( out_trade_no, "bar_code/wave_code", auth_code, subject, total_amount=12, discountable_amount=10 ) failed response = { "alipay_trade_pay_response": { "code": "40004", "msg": "Business Failed", "sub_code": "ACQ.INVALID_PARAMETER", "sub_msg": "", "buyer_pay_amount": "0.00", "invoice_amount": "0.00", "point_amount": "0.00", "receipt_amount": "0.00" }, "sign": "" } succeeded response = { "alipay_trade_pay_response": { "trade_no": "2017032121001004070200176846", "code": "10000", "invoice_amount": "20.00", "open_id": "20880072506750308812798160715407", "fund_bill_list": [ { "amount": "20.00", "fund_channel": "ALIPAYACCOUNT" } ], "buyer_logon_id": "csq***@sandbox.com", "receipt_amount": "20.00", "out_trade_no": "out_trade_no18", "buyer_pay_amount": "20.00", "buyer_user_id": "2088102169481075", "msg": "Success", "point_amount": "0.00", "gmt_payment": "2017-03-21 15:07:29", "total_amount": "20.00" }, "sign": "" } """ assert scene in ("bar_code", "wave_code"), 'scene not in ("bar_code", "wave_code")' biz_content = { "out_trade_no": out_trade_no, "scene": scene, "auth_code": auth_code, "subject": subject } biz_content.update(**kwargs) data = self.build_body("alipay.trade.pay", biz_content, notify_url=notify_url) response_type = "alipay_trade_pay_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_refund(self, refund_amount, out_trade_no=None, trade_no=None, **kwargs): biz_content = { "refund_amount": refund_amount } biz_content.update(**kwargs) if out_trade_no: biz_content["out_trade_no"] = out_trade_no if trade_no: biz_content["trade_no"] = trade_no data = self.build_body("alipay.trade.refund", biz_content) response_type = "alipay_trade_refund_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_cancel(self, out_trade_no=None, trade_no=None): """ response = { "alipay_trade_cancel_response": { "msg": "Success", "out_trade_no": "out_trade_no15", "code": "10000", "retry_flag": "N" } } """ assert (out_trade_no is not None) or (trade_no is not None), \ "Both trade_no and out_trade_no are None" biz_content = {} if out_trade_no: biz_content["out_trade_no"] = out_trade_no if trade_no: biz_content["trade_no"] = trade_no data = self.build_body("alipay.trade.cancel", biz_content) response_type = "alipay_trade_cancel_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_close(self, out_trade_no=None, trade_no=None, operator_id=None): """ response = { "alipay_trade_close_response": { "code": "10000", "msg": "Success", "trade_no": "2013112111001004500000675971", "out_trade_no": "YX_001"a } } """ assert (out_trade_no is not None) or (trade_no is not None), \ "Both trade_no and out_trade_no are None" biz_content = {} if out_trade_no: biz_content["out_trade_no"] = out_trade_no if trade_no: biz_content["trade_no"] = trade_no if operator_id: biz_content["operator_id"] = operator_id data = self.build_body("alipay.trade.close", biz_content) response_type = "alipay_trade_close_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_create( self, subject, out_trade_no, total_amount, notify_url=None, **kwargs ): biz_content = { "subject": subject, "out_trade_no": out_trade_no, "total_amount": total_amount } biz_content.update(kwargs) data = self.build_body("alipay.trade.create", biz_content, notify_url=notify_url) response_type = "alipay_trade_create" return self.verified_sync_response(data, response_type) def api_alipay_trade_precreate( self, subject, out_trade_no, total_amount, notify_url=None, **kwargs ): """ success response = { "alipay_trade_precreate_response": { "msg": "Success", "out_trade_no": "out_trade_no17", "code": "10000", "qr_code": "https://qr.alipay.com/bax03431ljhokirwl38f00a7" }, "sign": "" } failed response = { "alipay_trade_precreate_response": { "msg": "Business Failed", "sub_code": "ACQ.TOTAL_FEE_EXCEED", "code": "40004", "sub_msg": "订单金额超过限额" }, "sign": "" } """ biz_content = { "out_trade_no": out_trade_no, "total_amount": total_amount, "subject": subject } biz_content.update(**kwargs) data = self.build_body("alipay.trade.precreate", biz_content, notify_url=notify_url) response_type = "alipay_trade_precreate_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_fastpay_refund_query( self, out_request_no, trade_no=None, out_trade_no=None ): assert (out_trade_no is not None) or (trade_no is not None), \ "Both trade_no and out_trade_no are None" biz_content = {"out_request_no": out_request_no} if trade_no: biz_content["trade_no"] = trade_no else: biz_content["out_trade_no"] = out_trade_no data = self.build_body("alipay.trade.fastpay.refund.query", biz_content) response_type = "alipay_trade_fastpay_refund_query_response" return self.verified_sync_response(data, response_type) def api_alipay_fund_trans_toaccount_transfer( self, out_biz_no, payee_type, payee_account, amount, **kwargs ): assert payee_type in ("ALIPAY_USERID", "ALIPAY_LOGONID"), "unknown payee type" biz_content = { "out_biz_no": out_biz_no, "payee_type": payee_type, "payee_account": payee_account, "amount": amount } biz_content.update(kwargs) data = self.build_body("alipay.fund.trans.toaccount.transfer", biz_content) response_type = "alipay_fund_trans_toaccount_transfer_response" return self.verified_sync_response(data, response_type) def api_alipay_fund_trans_order_query(self, out_biz_no=None, order_id=None): if out_biz_no is None and order_id is None: raise Exception("Both out_biz_no and order_id are None!") biz_content = {} if out_biz_no: biz_content["out_biz_no"] = out_biz_no if order_id: biz_content["order_id"] = order_id data = self.build_body("alipay.fund.trans.order.query", biz_content) response_type = "alipay_fund_trans_order_query_response" return self.verified_sync_response(data, response_type) def api_alipay_trade_order_settle( self, out_request_no, trade_no, royalty_parameters, **kwargs ): biz_content = { "out_request_no": out_request_no, "trade_no": trade_no, "royalty_parameters": royalty_parameters, } biz_content.update(kwargs) data = self.build_body("alipay.trade.order.settle", biz_content) response_type = "alipay_trade_order_settle_response" return self.verified_sync_response(data, response_type) def api_alipay_ebpp_invoice_token_batchquery(self, invoice_token=None, scene=None): if scene is None: scene = "INVOICE_EXPENSE" if invoice_token is None: raise Exception("invoice_token is None!") biz_content = { "invoice_token": invoice_token, "scene": scene } data = self.build_body("alipay.ebpp.invoice.token.batchquery", biz_content) response_type = "alipay_ebpp_invoice_token_batchquery_response" return self.verified_sync_response(data, response_type) def _verify_and_return_sync_response(self, raw_string, response_type): """ return response if verification succeeded, raise exception if not As to issue #69, json.loads(raw_string)[response_type] should not be returned directly, use json.loads(plain_content) instead failed response is like this { "alipay_trade_query_response": { "sub_code": "isv.invalid-app-id", "code": "40002", "sub_msg": "无效的AppID参数", "msg": "Invalid Arguments" } } """ response = json.loads(raw_string) # raise exceptions if "sign" not in response.keys(): result = response[response_type] raise AliPayException( code=result.get("code", "0"), message=raw_string ) sign = response["sign"] # locate string to be signed plain_content = self._get_string_to_be_signed(raw_string, response_type) if not self._verify(plain_content, sign): raise AliPayValidationError return json.loads(plain_content) def verified_sync_response(self, data, response_type): url = self._gateway + "?" + self.sign_data(data) raw_string = urlopen(url, timeout=self._config.timeout).read().decode() return self._verify_and_return_sync_response(raw_string, response_type) def _get_string_to_be_signed(self, raw_string, response_type): """ https://docs.open.alipay.com/200/106120 从同步返回的接口里面找到待签名的字符串 """ balance = 0 start = end = raw_string.find("{", raw_string.find(response_type)) # 从response_type之后的第一个{的下一位开始匹配, # 如果是{则balance加1; 如果是}而且balance=0,就是待验签字符串的终点 for i, c in enumerate(raw_string[start + 1:], start + 1): if c == "{": balance += 1 elif c == "}": if balance == 0: end = i + 1 break balance -= 1 return raw_string[start:end] class AliPay(BaseAliPay): pass class DCAliPay(BaseAliPay): """ 数字证书 (digital certificate) 版本 """ def __init__( self, appid, app_notify_url, app_private_key_string, app_public_key_cert_string, alipay_public_key_cert_string, alipay_root_cert_string, sign_type="RSA2", debug=False ): """ 初始化 DCAlipay( appid='', app_notify_url='http://example.com', app_private_key_string='', app_public_key_cert_string='', alipay_public_key_cert_sring='', aplipay_root_cert_string='', ) """ self._app_public_key_cert_string = app_public_key_cert_string self._alipay_public_key_cert_string = alipay_public_key_cert_string self._alipay_root_cert_string = alipay_root_cert_string alipay_public_key_string = self.load_alipay_public_key_string() super().__init__( appid=appid, app_notify_url=app_notify_url, app_private_key_string=app_private_key_string, alipay_public_key_string=alipay_public_key_string, sign_type=sign_type, debug=debug ) def api_alipay_open_app_alipaycert_download(self, alipay_cert_sn): """ 下载支付宝证书 验签使用,支付宝公钥证书无感知升级机制 """ biz_content = { "alipay_cert_sn": alipay_cert_sn } data = self.build_body("alipay.open.app.alipaycert.download", biz_content) return self.sign_data(data) def build_body(self, *args, **kwargs): data = super().build_body(*args, **kwargs) data["app_cert_sn"] = self.app_cert_sn data["alipay_root_cert_sn"] = self.alipay_root_cert_sn if self._verbose: logger.debug("data to be signed") logger.debug(data) return data def load_alipay_public_key_string(self): cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, self._alipay_public_key_cert_string ) return OpenSSL.crypto.dump_publickey( OpenSSL.crypto.FILETYPE_PEM, cert.get_pubkey() ).decode("utf-8") @staticmethod def get_cert_sn(cert): """ 获取证书 SN 算法 """ cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) certIssue = cert.get_issuer() name = 'CN={},OU={},O={},C={}'.format(certIssue.CN, certIssue.OU, certIssue.O, certIssue.C) string = name + str(cert.get_serial_number()) return hashlib.md5(string.encode()).hexdigest() @staticmethod def read_pem_cert_chain(certContent): """解析根证书""" # 根证书中,每个 cert 中间有两个回车间隔 items = [i for i in certContent.split('\n\n') if i] load_cert = partial(OpenSSL.crypto.load_certificate, OpenSSL.crypto.FILETYPE_PEM) return [load_cert(c) for c in items] @staticmethod def get_root_cert_sn(rootCert): """ 根证书 SN 算法""" certs = DCAliPay.read_pem_cert_chain(rootCert) rootCertSN = None for cert in certs: try: sigAlg = cert.get_signature_algorithm() except ValueError: continue if sigAlg in CryptoAlgSet: certIssue = cert.get_issuer() name = 'CN={},OU={},O={},C={}'.format( certIssue.CN, certIssue.OU, certIssue.O, certIssue.C ) string = name + str(cert.get_serial_number()) certSN = hashlib.md5(string.encode()).hexdigest() if not rootCertSN: rootCertSN = certSN else: rootCertSN = rootCertSN + '_' + certSN return rootCertSN @property def app_cert_sn(self): if not hasattr(self, "_app_cert_sn"): self._app_cert_sn = self.get_cert_sn(self._app_public_key_cert_string) return getattr(self, "_app_cert_sn") @property def alipay_root_cert_sn(self): if not hasattr(self, "_alipay_root_cert_sn"): self._alipay_root_cert_sn = self.get_root_cert_sn(self._alipay_root_cert_string) return getattr(self, "_alipay_root_cert_sn") def api_alipay_fund_trans_uni_transfer( self, out_biz_no, identity_type, identity, trans_amount, name=None, **kwargs ): """ 单笔转账接口, 只支持公钥证书模式 文档地址: https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer """ assert identity_type in ("ALIPAY_USER_ID", "ALIPAY_LOGON_ID"), "unknown identity type" biz_content = { "payee_info": { "identity": identity, "identity_type": identity_type, }, "out_biz_no": out_biz_no, "trans_amount": trans_amount, "product_code": "TRANS_ACCOUNT_NO_PWD", "biz_scene": "DIRECT_TRANSFER", } biz_content["payee_info"]["name"] = name if name else None biz_content.update(kwargs) response_type = "alipay_fund_trans_uni_transfer_response" data = self.build_body("alipay.fund.trans.uni.transfer", biz_content) return self.verified_sync_response(data, response_type) class ISVAliPay(BaseAliPay): def __init__( self, appid, app_notify_url, app_private_key_string=None, alipay_public_key_string=None, sign_type="RSA2", debug=False, app_auth_token=None, app_auth_code=None ): if not app_auth_token and not app_auth_code: raise Exception("Both app_auth_code and app_auth_token are None !!!") self._app_auth_token = app_auth_token self._app_auth_code = app_auth_code super().__init__( appid, app_notify_url, app_private_key_string=app_private_key_string, alipay_public_key_string=alipay_public_key_string, sign_type=sign_type, debug=debug ) @property def app_auth_token(self): # 没有则换取token if not self._app_auth_token: result = self.api_alipay_open_auth_token_app(self._app_auth_code) self._app_auth_token = result.get("app_auth_token") if not self._app_auth_token: msg = "Get auth token by auth code failed: {}" raise Exception(msg.format(self._app_auth_code)) return self._app_auth_token def build_body(self, *args, **kwargs): data = super().build_body(*args, **kwargs) if self._app_auth_token: data["app_auth_token"] = self._app_auth_token if self._verbose: logger.debug("data to be signed") logger.debug(data) return data def api_alipay_open_auth_token_app(self, refresh_token=None): """ response = { "code": "10000", "msg": "Success", "app_auth_token": "201708BB28623ce3d10f4f62875e9ef5cbeebX07", "app_refresh_token": "201708BB108a270d8bb6409890d16175a04a7X07", "auth_app_id": "appid", "expires_in": 31536000, "re_expires_in": 32140800, "user_id": "2088xxxxx } """ if refresh_token: biz_content = { "grant_type": "refresh_token", "refresh_token": refresh_token } else: biz_content = { "grant_type": "authorization_code", "code": self._app_auth_code } data = self.build_body( "alipay.open.auth.token.app", biz_content, ) response_type = "alipay_open_auth_token_app_response" return self.verified_sync_response(data, response_type) def api_alipay_open_auth_token_app_query(self): biz_content = {"app_auth_token": self.app_auth_token} data = self.build_body( "alipay.open.auth.token.app.query", biz_content, ) response_type = "alipay_open_auth_token_app_query_response" return self.verified_sync_response(data, response_type)