├── README.md ├── __init__.py ├── alipay.py ├── alipay_config.py ├── alipay_core.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | #支付宝移动支付的python实现。希望能帮助大家快速接入支付宝快捷支付。 2 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | -------------------------------------------------------------------------------- /alipay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | from tornado import gen 4 | from tornado import web 5 | import alipay_config 6 | import alipay_core 7 | from alipay_core import * 8 | import json 9 | import datetime 10 | 11 | 12 | host = "http://baidu.com" # 这里改为你自己的host 13 | class MakePaymentInfo(web.RequestHandler): 14 | """ 15 | 构造一个支付请求 16 | """ 17 | def make_payment_info(self, out_trade_no=None, subject=None, total_amount=None, body=None, passback_params=None): 18 | public = { # public args 19 | "app_id": alipay_config.appid, 20 | "method": "alipay.trade.app.pay", 21 | "charset": "utf-8", 22 | "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), # 2014-07-24 03:07:50 23 | "version": "1.0", 24 | "notify_url": "https://www.your-callback-url.com/callback/alipay", 25 | "sign_type": "RSA2" 26 | } 27 | 28 | order_info = {"product_code": "QUICK_MSECURITY_PAY", 29 | # 业务参数 30 | "out_trade_no": None, 31 | "subject": None, 32 | "total_amount": total_amount, 33 | "body": body, 34 | } 35 | if not passback_params: 36 | order_info['passback_params'] = passback_params 37 | order_info["out_trade_no"] = "%s" % (out_trade_no) 38 | order_info["subject"] = "%s" % (subject) 39 | if total_amount <= 0.0: 40 | total_amount = 0.01 41 | order_info["total_amount"] = "%s" % (total_amount) 42 | 43 | public['biz_content'] = json.dumps(order_info, ensure_ascii=False) 44 | return public 45 | 46 | @gen.coroutine 47 | def get(self, order_id): 48 | """ 49 | 这里是客户端请求服务端来获取提交给支付宝的支付请求, 因为私钥放在客户端来签名的话,可能会遭遇破解,所以构造了这个服务端用来签名的方法 50 | """ 51 | self.set_status(200) 52 | self.set_header('Content-Type', 'application/json; charset=UTF-8') 53 | 54 | #构造订单信息 55 | STATUS_SUCCESS = 0 #成功 56 | STATUS_PAYED = 1 #订单已支付 57 | STAUTS_ORDER_NOT_EXTED = 2 # 订单不存在 58 | STATUS_ORDER_REFUND = 3 #订单已退款 59 | STATUS_ORDER_STATUS_ERROR = 4 #订单状态错误 60 | 61 | 62 | 63 | 64 | total_fee = 0.01 #这里将金额设为1分钱,方便测试 65 | body = "" 66 | payment_info = self.make_payment_info(out_trade_no=order_id, subject=order_id, total_fee=total_fee, 67 | body=body) 68 | res = alipay_core.make_payment_request(payment_info) 69 | self.write({"status": STATUS_SUCCESS, "res": res}) 70 | 71 | 72 | class PaymentCallBack(web.RequestHandler): 73 | """ 74 | 阿里支付回调 75 | """ 76 | """ 77 | WAIT_BUY 78 | body=商品描述&buyer_email=ma.hongwei@foxmail.com&buyer_id=2088702056383644&discount=0.00&gmt_create=2015-07-10 18:06:54&is_total_fee_adjust=Y¬ify_id=cf579a7403c6bdc806ccb49101510aa05k¬ify_time=2015-07-10 18:06:54¬ify_type=trade_status_sync&out_trade_no=PRH0BL7A6QLCSUO&payment_type=1&price=0.01&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=Lp3JJj4WpR1BggPiC/O0IPGGeIc5xxPLhA7ICvEYElaEv820Ju8GeFXKjepvIlgWa76lMMGW99BPW0HkmqvPRE1thJevKNHQ+KQTIXpw5iKaoXbAXeURrs4LUK6CTIQBpX0fAjfZ4EMAHnx31fgRnJbuuWCOTJ8MLgjLvDmJs9M=&sign_type=RSA&subject=商品测试&total_fee=0.01&trade_no=2015071000001000640055508270&trade_status=WAIT_BUYER_PAY&use_coupon=N 79 | 80 | PAYSUCCESS 81 | body=商品描述&buyer_email=ma.hongwei@foxmail.com&buyer_id=2088702056383644&discount=0.00&gmt_create=2015-07-10 18:06:54&gmt_payment=2015-07-10 18:06:55&is_total_fee_adjust=N¬ify_id=f00d4988288e8e4af8bff37f4b0e365b5k¬ify_time=2015-07-10 18:06:55¬ify_type=trade_status_sync&out_trade_no=PRH0BL7A6QLCSUO&payment_type=1&price=0.01&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=CfTvBdJ6blsg0BjDLq51eJn073VkVkoK79kTFOURuN4Av48lvJxN4S4lqm+sRW6xA7AlKBm/pNKqz0eRT1jzFvaG8Nd4C2vMKYzzQ/CrTkDym74od+q04Sdp2X+xB5fKfqeOy3qyFJgpXX1WT7npXjWqAxmT80ZTQiykoufWnNo=&sign_type=RSA&subject=商品测试&total_fee=0.01&trade_no=2015071000001000640055508270&trade_status=TRADE_SUCCESS&use_coupon=N 82 | 83 | """ 84 | 85 | #这里需要做3件事情 86 | #1验证平台签名 87 | #2去阿里验证回调信息是否真实 88 | #3执行业务逻辑,回调订单状态 89 | 90 | @gen.coroutine 91 | def post(self): 92 | args = self.request.arguments 93 | for k, v in args.items(): 94 | args[k] = v[0] 95 | 96 | check_sign = params_to_query(args) 97 | params = query_to_dict(check_sign) 98 | sign = params['sign'] 99 | toSignDict = {} 100 | for k, v in params.items(): 101 | if k == 'sign' or k == 'sign_type': 102 | continue 103 | str = urllib.parse.unquote_plus(v) 104 | toSignDict[k] = str 105 | # params = params_filter(params) 106 | message = params_to_query(params,quotes=False,reverse=False) #获取到要验证签名的串 107 | check_res = check_ali_sign(message,sign) #验签 108 | 109 | if check_res == False: 110 | self.write("fail") 111 | return 112 | 113 | #这里是去访问支付宝来验证订单是否正常 114 | # res = verify_from_gateway({"partner": alipay_config.partner_id, "notify_id": params["notify_id"]}) 115 | # if res == False: 116 | # self.write("fail") 117 | # return 118 | 119 | trade_status = params["trade_status"] 120 | order_id = params["out_trade_no"] #你自己构建订单时候的订单ID 121 | alipay_order = params["trade_no"] #支付宝的订单号码 122 | total_fee = params["total_amount"] #支付总额 123 | 124 | """ 125 | 下面是处理付款完成的逻辑 126 | """ 127 | if trade_status == "TRADE_SUCCESS": #交易成功 128 | #TODO:这里来做订单付款后的操作 129 | self.write("success") 130 | return 131 | if trade_status == "TRADE_FINISHED": 132 | return 133 | 134 | if trade_status == "WAIT_BUYER_PAY": 135 | 136 | self.write("success") 137 | return 138 | if trade_status == "TRADE_CLOSED":#退款会回调这里 139 | self.write("success") 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /alipay_config.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | 3 | 4 | """ 5 | 注意,这里面的数据为了安全性,其实我都篡改了一些字母,是错的,但是大家配置的适合按照这个格式来配置,尤其是RSA_ALIPAY_PUBLIC 6 | """ 7 | 8 | RSA_ALIPAY_PUBLIC = """-----BEGIN PUBLIC KEY----- 9 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRAFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQEB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5KsiNG9zpgmLCUYuLkxpLQIDAQAB 10 | -----END PUBLIC KEY-----""" 11 | 12 | #商户私钥 13 | RSA_PRIVATE ="""-----BEGIN RSA PRIVATE KEY----- 14 | MIICXAIBAAKBgQCslpBgJhEFGG54Dn9oTz4IVA4D2F4/mx+IinE783+N9g2QNWVX 15 | yL4DgyDnGfpEtG+mh9wqR+AjFKLnAWwoOPZb0lDeHyrBviUuoj6SlrGEKYbx3Mc0 16 | wCq3CJKKGR4ELxNMXgdbBJP0FK7K3qLqQRIqPeCkSrwUAhZL/6pPYu36zwIDAQAB 17 | AoGBAIj32M6lhz5e/FTFVLHIqbdcVd8RWQLrriPjkyf8AA/+5Ra8eZgoVTxeVb9y 18 | q17ZP/6OtdjMQAI3P21LgLWPN1urjVOKn20RMgN5gTwFtx0LmplelxIKi9+sAuoz 19 | CxzoejceqyI0tub3J29fw+klOh1UhMCxXnoOCwRRbuqeoWmZAkEA38LgSljePkYs 20 | MSJmpgAobMiwHQsuh63KLR3/tb7Oqp6GaTr9wPwnAEYRoEs7s43laqUV1ZKOFz3P 21 | 5/8e9BzQYwJBAMV0PS2dEmOAB9f7ZewKcsicqgBtc2NsXxBVruy1vlyi4od7yox6 22 | WqgpZOOBQ34wLFHzNhtFLY3umQCVTPaWGaUCQE29VdFeuK/oD1huvDz2FxVcUBt3 23 | FA5dab1tC7cn+IM8mjQbI6gVsrMcpOkM2kwSg18excwyo50dg4r1jtp+3EMCQGRE 24 | osyG9s+QNGt5PkifBarZ43pUGkRiw+OHn0yU/hYMoPwr4K91cpPycUkWWy+5RshW 25 | uSMf/gIVJ50kM/EESAECQGaTj1714Fe1uomYNClkTluZDPF06d6c7y78R3c7QhxA 26 | kKlCsuU/zb5WcX3oI0TxUS5mVuGWMEWvq/zJHplDZR4= 27 | -----END RSA PRIVATE KEY-----""" 28 | 29 | RSA_PUBLIC = """-----BEGIN PUBLIC KEY----- 30 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCslpBgJhEFGG54Dn9oTz4IVA4D 31 | 2F4/mx+IinE783+N9g2QNWVXyL4DgyDnGfpEtG+mh9wqR+AjFKLnAWwoOPZb0lDe 32 | HyrBviUuoj6SlrGEKYbx3Mc0wCq3CJKKGR4ELxNMXgdbBJP0FK7K3qLqQRIqPeCk 33 | SrwUAhZL/6pPYu36zwIDAQAB 34 | -----END PUBLIC KEY-----""" 35 | 36 | partner_id = 12345678 #支付宝后台的合作商ID 37 | key = "x6lmikee5cfkpv0sicx3i24qpa3yggdk" 38 | appid="2017060000003217" #your appid 39 | 40 | -------------------------------------------------------------------------------- /alipay_core.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | __author__ = 'zhanglong' 3 | 4 | import rsa 5 | import alipay_config 6 | import base64 7 | 8 | SIGN_TYPE = "SHA-256" 9 | import urllib 10 | import requests 11 | import hashlib 12 | import urllib.parse 13 | 14 | def params_filter(params): 15 | """ 16 | 去掉不需要验证前面的参数 17 | :param params: 18 | :return: 19 | """ 20 | """ 21 | :param params: 22 | :return: 23 | """ 24 | ret = {} 25 | for key, value in params.items(): 26 | if key == "sign" or value == "":#sign_type need include to sign now 27 | continue 28 | ret[key] = value 29 | return ret 30 | 31 | def params_escape(dict): 32 | escapedDict = {} 33 | for key, value in dict.items(): 34 | str = urllib.quote_plus(value) 35 | escapedDict[key] = str 36 | return escapedDict 37 | 38 | def query_to_dict(query): 39 | """ 40 | 将query string转换成字典 41 | :param query: 42 | :return: 43 | """ 44 | res = {} 45 | k_v_pairs = query.split("&") 46 | for item in k_v_pairs: 47 | sp_item = item.split("=", 1) #注意这里,因为sign秘钥里面肯那个包含'='符号,所以splint一次就可以了 48 | key = sp_item[0] 49 | value = sp_item[1] 50 | res[key] = value 51 | 52 | return res 53 | 54 | 55 | def params_to_query(params, quotes=False, reverse=False): 56 | """ 57 | 生成需要签名的字符串 58 | :param params: 59 | :return: 60 | """ 61 | """ 62 | :param params: 63 | :return: 64 | """ 65 | query = "" 66 | for key in sorted(params.keys(), reverse=reverse): 67 | value = params[key] 68 | if quotes == True: 69 | query += str(key) + "=\"" + str(value) + "\"&" 70 | else: 71 | query += str(key) + "=" + str(value) + "&" 72 | query = query[0:-1] 73 | return query 74 | 75 | 76 | def make_sign(message): 77 | """ 78 | 签名 79 | :param message: 80 | :return: 81 | """ 82 | private_key = rsa.PrivateKey._load_pkcs1_pem(alipay_config.RSA_PRIVATE) 83 | sign = rsa.sign(message, private_key, SIGN_TYPE) 84 | b64sing = base64.b64encode(sign) 85 | return b64sing 86 | 87 | def make_md5_sign(message): 88 | m = hashlib.md5() 89 | m.update(message) 90 | m.update(alipay_config.key) 91 | return m.hexdigest() 92 | 93 | 94 | def check_sign(message, sign): 95 | """ 96 | 验证自签名 97 | :param message: 98 | :param sign: 99 | :return: 100 | """ 101 | sign = base64.b64decode(sign) 102 | pubkey = rsa.PublicKey.load_pkcs1_openssl_pem(alipay_config.RSA_PUBLIC) 103 | return rsa.verify(message, sign, pubkey) 104 | 105 | 106 | def check_ali_sign(message, sign): 107 | """ 108 | 验证ali签名 109 | :param message: 110 | :param sign: 111 | :return: 112 | """ 113 | sign = base64.b64decode(sign) 114 | pubkey = rsa.PublicKey.load_pkcs1_openssl_pem(alipay_config.RSA_ALIPAY_PUBLIC) 115 | res = False 116 | try: 117 | res = rsa.verify(message, sign, pubkey) 118 | except Exception as e: 119 | print e 120 | res = False 121 | return res 122 | 123 | 124 | def make_payment_request(params_dict): 125 | """ 126 | 构造一个支付请求的信息,包含最终结果里面包含签名 127 | :param params_dict: 128 | :return: 129 | """ 130 | query_str = params_to_query(params_dict, quotes=True) #拼接签名字符串 131 | sign = make_sign(query_str) #生成签名 132 | res = "%s&sign=%s" % (query_str, sign) 133 | dict = query_to_dict(res) 134 | dict = params_escape(dict) 135 | res = params_to_query(dict) 136 | return res 137 | 138 | 139 | 140 | 141 | def verify_alipay_request_sign(params_dict): 142 | """ 143 | 验证阿里支付回调接口签名 144 | :param params_dict: 阿里回调的参数列表 145 | :return:True or False 146 | """ 147 | sign = params_dict['sign'] 148 | params = params_filter(params_dict) 149 | message = params_to_query(params, quotes=False, reverse=False) 150 | check_res = check_ali_sign(message, sign) 151 | return check_res 152 | 153 | 154 | def verify_from_gateway(params_dict): 155 | """ 156 | 从阿里网关验证请求是否正确 157 | :param params_dict: 158 | :return: 159 | """ 160 | ali_gateway_url = "https://mapi.alipay.com/gateway.do?service=notify_verify&partner=%(partner)d¬ify_id=%(notify_id)s" 161 | notify_id = params_dict["notify_id"] 162 | partner = alipay_config.partner_id 163 | ali_gateway_url = ali_gateway_url % {"partner": partner, "notify_id": notify_id} 164 | res = requests.get(ali_gateway_url) 165 | # res_dict = encoder.XML2Dict.parse(res.text) 166 | if res.text == "true": 167 | return True 168 | return False 169 | 170 | 171 | #test 172 | def test(): 173 | params = {"a": 1, "b": 2, "c": "", 1: 1, "sign": "asdfasdfas", "sign_type": "rsa"} 174 | after_params = params_filter(params) 175 | assert after_params == {"a": 1, "b": 2, 1: 1} 176 | query = params_to_query(after_params) 177 | 178 | assert query == '1=1&a=1&b=2' 179 | print query 180 | 181 | query2 = params_to_query(after_params, quotes=True) 182 | assert query2 == '1="1"&a="1"&b="2"' 183 | print query2 184 | 185 | sign = make_sign(query) 186 | #print sign 187 | sign_res = check_sign(query, sign) 188 | assert sign_res == True 189 | 190 | check_signa = "body=商品描述&buyer_email=zhanglwork@gmail.com&buyer_id=2088102716951071&discount=0.00&gmt_create=2015-07-13 10:28:00&gmt_payment=2015-07-13 10:28:01&is_total_fee_adjust=N¬ify_id=83ee2b993b46d3f5d1d27b9078199d062e¬ify_time=2015-07-13 10:28:01¬ify_type=trade_status_sync&out_trade_no=1WZ6ZYT9VYCTLN6&payment_type=1&price=0.01&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=H5VGZ63LYr3f9819ABuBuaxzRVOx5u3Ku3BI661jkW5gisD1XMc4PdV6bfI/5EIEFQvmSKLADYG3I/8N8Ty5eu/xsrcQXjsVC3Zr3wLOXaDnYh8Ale2crDoIQjgUrbg4d8csovBrJV9Fi+/SCM2/EXPxlO0qrilY/EpKYOczzZ8=&sign_type=RSA&subject=商品测试&total_fee=0.01&trade_no=2015071300001000070073886063&trade_status=TRADE_SUCCESS&use_coupon=N" 191 | 192 | """ 193 | payment_url body=商品描述&buyer_email=zhanglwork@gmail.com&buyer_id=2088102716951071&discount=0.00&gmt_create=2015-07-13 17:10:23&is_total_fee_adjust=Y¬ify_id=a4b691fca6f6f79e816fe022e398d2532e¬ify_time=2015-07-13 17:10:23¬ify_type=trade_status_sync&out_trade_no=OWYNN72PRTU2G81&payment_type=1&price=0.10&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=oX0I0LR7YGH96d3fZY9MfLj7BlAWclwVR3kK5XMgmoWcjFTpclB6tXssL81a+JOsKP0bcPsbH3dygMjjCVZHnHOpArs0tzLutjj00XnqH8uXEAItTPs2Hf/ld3TIZqsdXYBfVHtZaiPko/CgN8VQwjjITW1IRIY5JTE/MWidE8A=&sign_type=RSA&subject=商品测试&total_fee=0.10&trade_no=2015071300001000070073922535&trade_status=WAIT_BUYER_PAY&use_coupon=N 194 | [I 150713 17:10:23 web:1728] 200 POST /consumer/api/v1/alipay_callback (127.0.0.1) 1.21ms 195 | payment_url body=商品描述&buyer_email=zhanglwork@gmail.com&buyer_id=2088102716951071&discount=0.00&gmt_create=2015-07-13 17:10:23&gmt_payment=2015-07-13 17:10:24&is_total_fee_adjust=N¬ify_id=4c8d6808aac61d3ead5cd1bb187374e72e¬ify_time=2015-07-13 17:10:24¬ify_type=trade_status_sync&out_trade_no=OWYNN72PRTU2G81&payment_type=1&price=0.10&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=KRffFX0OvuxQpusduxvMaJ0f6sVmY8Ta8go969W+sKkypkt8SoUTXpJ5jjysa+/y8CTazBd+K+1Co9my/RswoDBjjspupLiuU0QcrNDDIBPPathk6tEhv8/16CF+IFbn1HoPKVjeHheuBLzCyiEdveqN7ORk/2T/Q9KG0Qqqs/I=&sign_type=RSA&subject=商品测试&total_fee=0.10&trade_no=2015071300001000070073922535&trade_status=TRADE_SUCCESS&use_coupon=N 196 | 197 | """ 198 | 199 | check_signa = "body=hsh_shop&buyer_email=ma.hongwei@foxmail.com&buyer_id=2088702056383644&discount=0.00&gmt_create=2015-07-28 19:56:11&is_total_fee_adjust=Y¬ify_id=1f124ba6791b3fadfe0734e9345b00b75k¬ify_time=2015-07-28 19:56:11¬ify_type=trade_status_sync&out_trade_no=1438084559&payment_type=1&price=0.01&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=WhHQeslDI0YtWdPmXIvvsB9KBZza5Mrzy1AldAq3f6cNP4ebMdMhoXLHAzu9oujm4UxOmTZX60/suYc7ciqea5LCsJR55yUlf4mxrHYqMkYr9+Xt2r/nKaEDe3AXEQHFl+KHYrNiPm35WCmz7rsiTv5p0X3SWK5YhEWT9ycYoHU=&sign_type=RSA&subject=corp_order&total_fee=0.01&trade_no=2015072800001000640056773075&trade_status=WAIT_BUYER_PAY&use_coupon=N" 200 | 201 | check_signa = "body=hsh_shop&buyer_email=ma.hongwei@foxmail.com&buyer_id=2088702056383644&discount=0.00&gmt_create=2015-07-28 19:56:11&gmt_payment=2015-07-28 19:56:11&is_total_fee_adjust=N¬ify_id=f6124d74bab89c36a7b3be29285110d25k¬ify_time=2015-07-28 19:56:11¬ify_type=trade_status_sync&out_trade_no=1438084559&payment_type=1&price=0.01&quantity=1&seller_email=xiaowenwen@7500.com.cn&seller_id=2088021072549071&sign=NFQvSMX3NPxRarKeQ4uJYfnL0z4Kx/bMvI6GbWJUNXRrEWHJ+PnLqiOCvy1EuO+doVBxiwL8acHOFSxyXBnevVG+2cq10YMTvTVet1ouhQNrL6WDZpqKC6TSjq8SRDvQooi9Kjeee4PuFJ6rnkFhRIeFebshLVi7MX3E3x+f808=&sign_type=RSA&subject=corp_order&total_fee=0.01&trade_no=2015072800001000640056773075&trade_status=TRADE_SUCCESS&use_coupon=N" 202 | 203 | params = query_to_dict(check_signa) 204 | sign = params['sign'] 205 | #sign = sign.decode('utf-8') 206 | params = params_filter(params) 207 | message = params_to_query(params, quotes=False, reverse=False) 208 | check_res = check_ali_sign(message, sign) 209 | assert check_res == True 210 | res = verify_from_gateway({"partner": alipay_config.partner_id, "notify_id": params["notify_id"]}) 211 | assert res == False 212 | 213 | 214 | def test_refund(): 215 | trade_order = "2015073000001000860056086838" 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rsa 2 | requests --------------------------------------------------------------------------------