github地址html
碼雲地址python
作了微信。支付寶和京東支付以後,發現,最扯蛋的支付,確定是京東支付,要完整開發京東支付,必需要看完京東支付開發者文檔的官網每個角落,絕對不能憑你的任何經驗去猜想有些流程,好比公私鑰加解密(不看官網,保證你後悔)、發送請求的方式(form表單提交,看了官網你會發現好怪異),支付同步跳轉(仍是post,fk),支付成功後返回竟然沒有支付訂單號(徹底靠本身去維護,fk)git
首先要去看官網的:http://payapi.jd.com/。 項目使用的是pc網頁支付github
一. 統一下單的接口:https://wepay.jd.com/jdpay/saveOrder算法
參數說明:http://payapi.jd.com/docList.html?methodName=2api
必定要仔細的看這些參數的說明數組
特殊參數說明以下:安全
二. 生成簽名服務器
簽名過程分爲兩步,首先是將原始參數按照規則拼接成一個字符串S1,而後再將S1根據簽名算法生成簽名字符串sign。 參數原始字符串的拼接規則:微信
生成簽名的過程以下:
注意事項:
簽名代碼:
def get_sign_str(params, is_compatible=False): """ 生成簽名的字符串 Args: params: 簽名的字典數據 is_compatible: 是不是兼容模式(對字典中value值爲空的也簽名) Returns: 返回簽名 """ raw = [(k, params[k]) for k in sorted(params.keys())] if is_compatible: order_str = "&".join("=".join(kv) for kv in raw) else: order_str = "&".join("=".join(kv) for kv in raw if kv[1]) return order_str def sign(self, prestr): """ 生成簽名 Args: prestr(str): 生成簽名的原字符串 Returns: 返回生成好的簽名 """ key = MRSA.load_key(self.MERCHANT_RSA_PRI_KEY) signature = key.private_encrypt(self.sha256(prestr), MRSA.pkcs1_padding) sign = base64.b64encode(signature) return sign
三. DES3對每一個參數進行加密(merchant(商戶號)、版本號(version)、簽名(sign)除外)
爲防止明文數據在post表單提交的時候暴露,因此京東作了DES3對字段進行加密(不用表單提交不就好了,還搞這麼複雜,真該學學支付寶和微信)
京東DES加密說明以下:
除特定說明外,商戶和京東支付接口調用報文采用3DES加密,再經過base64轉換爲字符串。 3DES加密算法標爲DESede,工做模式爲電子密碼本模式ECB,不填充(DESede/ECB/NoPadding)。 注:服務端NoPadding 爲不填充,因此加密的原文字節必須是8的整數倍(若是調用咱們提供的加密接口API則沒必要處理原文字節,加密接口內部已處理)。若是本身實現加密,原文字節不夠8的整數倍,則按以下規則轉爲8的整數倍。 1. 把原文字符串轉成字節數組。 2. 根據字節數組長度判斷是否須要補位。 補位邏輯爲: int x = (i+ 4) % 8; int y = (x == 0) ? 0 : (8 - x); i爲字節數組的長度,y爲須要補位的長度。 補位值爲0。 3. 將有效數據長度byte[]添加到原始byte數組的頭部。 i爲字節數組的長度。 result[0] = (byte) ((i >> 24) & 0xFF); result[1] = (byte) ((i >> 16) & 0xFF); result[2] = (byte) ((i >> 8) & 0xFF); result[3] = (byte) (i & 0xFF); 4. 原文字節數組前面加上第三步的4個字節,再加上需補位的值。 例如:字符串」1」,轉換成字節數組是[49],計算補位y=3, 計算有效數據長度爲[0, 0, 0, 1],最後字節數組爲[0, 0, 0, 1, 49, 0, 0, 0]。 Form表單接口的加密方式: 若是商戶經過表單方式提交支付請求至收銀臺,爲保證信息安全,表單中的各個字段除了merchant(商戶號)、verion(版本號)、sign(簽名)之外,其他字段所有采用3DES進行加密。 XML請求接口的加密方式: 經過XML接口方式和京東支付服務器交互的請求,應該對報文進行加密,加密方式爲對整個報文總體進行3DES加密,再進行base64轉碼使其變爲可讀字符串,加密後的密文置於<encrypt></encrypt>標籤中,同時再將報文中的<merchant>(商戶號)、<version>(版本號)這兩個字段單獨置於<jdpay>標籤下。 接收到京東支付加密報文後的處理方式: 接收到京東支付返回的加密報文後,先判斷<jdpay>標籤下的<result>標籤的返回碼,檢查接口調用是否正常返回。而後再讀取<encrypt>標籤的密文內容進行base64解碼,再進行3DES解密,解密後的報文便是原始報文。
示例代碼:
def des_pad(data): e = len(data) x = (e + 4) % 8 y = 0 if x == 0 else 8 - x sizeByte = struct.pack('>I', e) resultByte = range(len(sizeByte) + e + y) resultByte[0:4] = sizeByte resultByte[4:4 + e] = data for i in range(0, y): resultByte[e + 4 + i] = "\x00" resultstr = ''.join(resultByte) return resultstr def encode_des(to_encode_str, des_key): """ DES3加密數據 Args: to_encode_str(str): 要被加密的原字符串,這裏的字符串須要被des_pad一下 des_key(str): 加密的key Returns: """ key = base64.b64decode(des_key) des3 = DES3.new(key, DES3.MODE_ECB) return des3.encrypt(ToolsClass.des_pad(to_encode_str)).encode('hex_codec')
這樣的話,簽名和加密都已完成,日後就拼到頁面裏的form裏
<form method="post" action="https://wepay.jd.com/jdpay/saveOrder" id="batchForm"> <input name="merchant" type="hidden" id="merchant" value="22294531" /><br/> <input name="notifyUrl" type="hidden" id="notifyUrl" value="da652ac3b881c4ddc2ac26793b20c37fba91a994f108bf8a0a42b5ead05111997bfe2a97eaf4aa49562de1b6d1d32cd7" /><br/> <input name="userId" type="hidden" id="userId" value="f23f2b73027cb0f8deb349af3086fdc50f6892f17c9f45b81b6d273d0cdb1cae8151f083427fc8f0" /><br/> <input name="sign" type="hidden" id="sign" value="SJ6qfS+9CmXkt6ghJcf9nIdHJDReTFNkRyjFh5XZAsTAtfHT4SdmKeD88t+2dMnaszJ7vVjBnSu64aJyt6SODW2FHJk0WXEvZNixmo2h8F7vHO5lTE2jEG/9uN7sqg2c7kH2Fnu5cFLCeaMfb8uZqZ8CKi+g7Aw4b6rywvoH/8M=" /><br/> <input name="currency" type="hidden" id="currency" value="ac7132c57f10d3ce" /><br/> <input name="orderType" type="hidden" id="orderType" value="e00c693e6c5b8a60" /><br/> <input name="tradeNum" type="hidden" id="tradeNum" value="05439876d54534c7604c42eca17c14cdf8eece390982627a0799194a74809ee6c9d07d3cff8a7c60" /><br/> <input name="amount" type="hidden" id="amount" value="e5a6c3761ab9ddaf" /><br/> <input name="version" type="hidden" id="version" value="V2.0" /><br/> <input name="tradeTime" type="hidden" id="tradeTime" value="d9668085c69c2ecb33367c0710f42c4bc7432967ba39f140" /><br/> <input name="tradeName" type="hidden" id="tradeName" value="3e111657e2839e3a3ba10d54bb446817e5000daf14a2e3badbf9a93316ed6003" /><br/> <input name="callbackUrl" type="hidden" id="callbackUrl" value="51c916293675ac44c2ac26793b20c37fba91a994f108bf8a0a42b5ead05111997bfe2a97eaf4aa49229a23b8c688e767" /><br/><input type="submit" /> </form>
怎麼組織就本身去實現好了
四. 異步回調
提交以後請求以後,就會跳轉到京東的支付頁面,可登陸帳戶支付,也可用京東app或者微信掃描支付。
當用戶掃碼支付以後,京東會主動跳轉到你指定的一個網址(在提交支付請求的時候有這個字段),而且會異步post一個請求到指定的一個地址(在提交支付請求的時候有這個字段),同步跳轉是在用戶掃碼支付以後,若是京東支付頁面還在的話會跳轉。而異步是不管如何都會發支付結果通知的。對於新手來講,必定要知道這個行業潛規則(微信,支付寶or其它都是)。並且必定要以這個異步通知的結果爲準。
京東返回的是xml格式的字符串
返回格式以下(沒有換行的,我這裏演示換了行的):
<?xml version=\"1.0\" encoding=\"UTF-8\" ?> <jdpay> <version>V2.0</version> <merchant>22294531</merchant> <result> <code>000000</code> <desc>success</desc> </result> <encrypt>MWYxMjBjMzViZjgwOWM5ZDhjNjc0YmY1ZWJlY2QyODU0YTc5NmQ3ZWQxMWU1NzE3MWQ0OTUwOGI5NzllYmE4ZjM1YzRiZjlmYWE1M2ZiYjVmYzBmYTgyMDYyM2Q0YjM0NGM1ODFkZDhlYTA2Mjk0ZDE5ZDBlZDk5NTc3MmE4Nzk4OTFlYjIwZDgzMTc4MDU3NGVkZTFjNDY0MDMzNzNjZjc2OWZiMDQ0YjVhZGNhYmRhMGZmYTkyNzRhZDNhM2IxOGY5ZjZhYjBmYjhmZmI3Yzg0OTA3YzM0OGJmZTYwZTIzNzM3YjVmYzMzNmNkYTE0MjM2OWIwZDM5MjI2YWM5YmY3ZmZjZDBkNWJmM2ZkYWY4YTU3OWU4MDE3ZjQ5YmQ0ZWIyMDA0NTFmODZkNmViMDBiMDE2YTU3NTNjMzJjNDIzNWI5ZDkyYzQ3OTU4OTc2ZGIyZmNiMGUxNGRjNTM2OGZjYjQ0NmE0YWY1ZWVjZDYzNWI5ZDkyYzQ3OTU4OTc2NmIwM2QyZTU1ODJlNDNjM2M1NjA2YmQ5ZDc3MTRkMmNjN2ZiMDM3Yzg5ZDk1ODFkMWJhZmVjYjUwMzJlNTdkMTFmN2QxMDAxNjgyMzJjNTZhMmQzNTcyZGE4OTUzYWFjNTU5MDY4YWYyODE5ZDcyNmY5NmE1YTBmYWFiZTRiZTQ2OGZhMmM4M2JjMGM5NmNiMDE3ZWQ4MDkxY2FjZThiNzg4MjY5OWY1ZTJlYzBjOTIxODBhOGExNjExNGY4NWQwM2NkZjI2MTFmM2VmODcxYWM3MjUxZjMxMzZlYjFmNzI1NWE0OWM4MjMxZGY1MzBmY2Y1Mjg2NGUzMWRlMjc0M2I5ZDM5NjQzN2ZmZWQ1Y2M5NDY4ZDcwNWM1YzVhZmRlYzYwZWU3MDVhNjE0N2I1MGVlM2UyMGE2MzExNTE4YTUxOGRjMzBmMmUxZjE2NzYzNGRiNDJlODFmMDczOGYzZjMxN2NkMjkzNmU4ODc3NzJjMjkzM2ZlODlmMjUyNDVmNDI2MDA0M2VkYmUwOTlkNGEyNjU3YTM5YTE4ODU2OTBmNGQyNDcwZDE0ZWRjMmQxYjgxMzhhNjA5M2ZlNDkxYTQyMzE5YzBlNTA0MTdkYTg2ZGQ2NDQwODBmMjM4ZGI2YzIzMjNhOTE0M2VmMjZiZjczN2M5NWQwODYxMWY2OGE5MDQ0ZDZmNzE0NmIxZjQwZDdmZDMxOTQ2ZDM3YjIwNDJiODUzZGM0NTk0MzM5YzJkN2M2NDdiNGM4MzQ4MTRjZTIxZTlmYTYzNDYxNGMxMjlhZTE3NjE0ZDIzM2Q2MTQ4YzJiNWE3ZWVjMDU5MjFmNzJkNGNjNTU1NWZkNzVhN2U5Y2I1MDU1NjhlMWRlNjVhNzkyOGUxMThlODQyMGJkNzE2NjdmMDc3YmEyYTFkNmQyOTFiOGNjZTU2ZGMyYmE3ODY5ZGZiNmMyMWViYjc2ODc0Y2I3YTc4NGQ5NWY2NjY2Y2E5NjI0N2I1MGE4MTliMDBkNGIzNmViZTJlY2JmYTcwODUzYTM5ZTcwMDVmYWEzNWY2MDFhMWM2MGQ1MzEyYmQxNDU3Zjg4ZWVhNzY2YjZhOGE4ZGMxMGY3NjYwOWEzNWY2MDFhMWM2MGQ1MzFhNzA4NTNhMzllNzAwNWZhYTYxMmJmNjJiMmFlMGY5ODMxMzQ0MzQ0NjMxZDc3MTUyY2FiMjZlMjcyYmJjYmQzODVmNDY4OTA5YTdjMjlmNTI5NWFlZjE3NTI4ZmE4MzVhNzA4NTNhMzllNzAwNWZhNDk5OTQ2ZGU0OGU0NGQ2ZTE4YmRiYTBjZjNhM2ZkNjY5ODJjNGVhZjQzMjIyYWFhMWM0ZmU1ODRiNTg5OWEwYzAwNjI2NTllMDZkYzhiYTVmMjI3ZjUyYmQ3MjcyODllZmEwYzhiNDIwODc4ZjUzODY1MzAzZDkyNDM5OTRkNDczMTBjZDBhMTc4ZjAwOTIyZmM2ODk5YjkyYTJiODcwNjU4MzkzMzJkZWYzNDY1MzJlYTNiYTFhNjM0MWIwNjM4NjBjNjlmMzg1NWZjZWM5YWExMDdjZWY1MjkwZTZjMzgzOGYxNTRiNzFlN2E1YTczYWFkNzJlOTRiOWI3MmI2YWYyMTJjMjQ5Y2UzMmUxMGI4YWE0N2YzYzFmNjNiOGY4NjJlZmU1ZDM5NjcwODA3MGNjY2JjYWFkYjM3NzBmMGQzYjIyMGFmZTE3YWNjZWU1N2RmZTQxMzAxYjA2MDdlMg==</encrypt> </jdpay>
先要用DES3對encrypt節點裏的串進行解密
def un_des_pad(data): resultByte = data[0:4] e = struct.unpack('>I', resultByte)[0] x = (e + 4) % 8 y = 0 if x == 0 else 8 - x return data[4:] if y == 0 else data[4:-y] def decode_des(to_decode_str, des_key): """ 解密數據 Args: to_decode_str(str): 要解密的原字符串 des_key(str): 解密的key Returns: """ key = base64.b64decode(des_key) des3 = DES3.new(key, DES3.MODE_ECB) param = to_decode_str.decode("hex_codec") if to_decode_str is bytes else base64.b64decode(to_decode_str).decode( "hex_codec") param = des3.decrypt(param) return ToolsClass.un_des_pad(param) sign_begin = xml_data.find('<encrypt>') sign_end = xml_data.find('</encrypt>') encrypt_str = xml_data[sign_begin + 9:sign_end] xml_str = JdPay.decode_des(encrypt_str, deskey)
解密後的明文以下:
<?xml version="1.0" encoding="UTF-8" > <jdpay> <version>V2.0</version> <merchant>110290193003</merchant> <result> <code>000000</code> <desc>success</desc> </result> <device>6220</device> <sign>SJ6qfS+9CmXkt6ghJcf9nIdHJDReTFNkRyjFh5XZAsTAtfHT4SdmKeD88t+2dMnaszJ7vVjBnSu64aJyt6SODW2FHJk0WXEvZNixmo2h8F7vHO5lTE2jEG/9uN7sqg2c7kH2Fnu5cFLCeaMfb8uZqZ8CKi+g7Aw4b6rywvoH/8M=</sign> <tradeNum>201704250935156041484635</tradeNum> <tradeType>0</tradeType> <amount>3140</amount> <status>2</status> <payList> <pay> <payType>3</payType> <amount>1500</amount> <currency>CNY</currency> <tradeTime>20170425093516</tradeTime> </pay> <pay> <payType>1</payType> <amount>1640</amount> <currency>CNY</currency> <tradeTime>20170425093516</tradeTime> <detail> <cardHolderMobile>150****1596</cardHolderMobile> </detail> </pay> </payList> </jdpay>
解密以後就是驗證簽名是否正確,從上邊的串中拿到簽名和去除簽名以後的字符串
def verify_mysign(cls, sign, xml_str, jd_public_key): """ 驗證簽名 Args: sign: 簽名 xml_str: 去除簽名後的xml字符串 jd_public_key: 用於驗證的key Returns: """ xml_sha_str = SHA256.new(xml_str).hexdigest() key = MRSA.load_pub_key(jd_public_key) signature = key.public_decrypt(base64.standard_b64decode(sign), MRSA.pkcs1_padding) return signature == xml_sha_str
驗證經過以後再返回去除sign的xml字符串,並提取出裏邊的內容(詳情參數所表明的含義請看官方文檔)
五. 同步跳轉
同步跳轉就沒啥好說了,只是給個跳轉地址,可是這裏必定要注意,這個的是一個post請求(好像京東啥都喜歡post),而非微信或者支付寶或者other什麼的get請求。因此不要設置錯了
好了,到這裏一個完整的在線支付就完成了。這裏還要說明的是,涉及到加密和解密,就必定會有key,有DES3使用的對稱加密key,還有簽名使用的非對稱公鑰和私鑰。因此必定要配置好。 這裏個人源代碼裏用的都是京東提供的測試商戶號,還有一大推京東設置好的key,具體要去下載京東的【京東支付PC&H5接口文檔】,在文檔的最底部有賬號信息。
demo裏邊還有申請退款,申請撤單的接口,其實寫好一個接口的完成流程,別的流程都是直接套用就能夠了。
附帶一下博客地址