玩轉京東支付(python)

oldsyang發表於2019-02-16

說明

github地址

做了微信。支付寶和京東支付之後,發現,最扯蛋的支付,肯定是京東支付,要完整開發京東支付,必須要看完京東支付開發者文件的官網每一個角落,絕對不能憑你的任何經驗去猜測有些流程,比如公私鑰加解密(不看官網,保證你後悔)、傳送請求的方式(form表單提交,看了官網你會發現好怪異),支付同步跳轉(還是post,fk),支付成功後返回居然沒有支付訂單號(完全靠自己去維護,fk)

技術描點

首先要去看官網的:http://payapi.jd.com/。 專案使用的是pc網頁支付

一. 統一下單的介面:https://wepay.jd.com/jdpay/sa…

引數說明:http://payapi.jd.com/docList….

一定要仔細的看這些引數的說明

特殊引數說明如下:

1) 在以上的請求引數中,商戶號是在註冊開通京東支付功能的時候,京東支付商戶管理系統為使用者分配的。
2) 使用者賬號是商戶系統的使用者賬號。
3) 交易流水號是用來標識每次支付請求的號碼,需要商戶保證在每一次支付請求的時候交易流水號唯一,多次請求不能使用同一交易流水號,否則京東支付服務在處理後面的支付請求時,會把此交易當做重複支付處理。
4) 簽名規則詳見:“介面安全規範-簽名演算法”;
5) 為保證資訊保安,表單中的各個欄位除了merchant(商戶號)、版本號(version)、簽名(sign)以外,其餘欄位全部採用3DES進行加密。

二. 生成簽名

簽名過程分為兩步,首先是將原始引數按照規則拼接成一個字串S1,然後再將S1根據簽名演算法生成簽名字串sign。
引數原始字串的拼接規則:

1) 對於POST表單提交的引數:所有引數按照引數名的ASCII碼順序從小到大排序(字典序),使用URL鍵值對的方式拼接成字串S1,(如:k1=value1&k2=value2&k3=value3…)
2) 對於XML報文互動的引數:將XML報文的各行去掉空格後直接拼接成一行字串作為S1。如果報文只有一行則直接作為S1,不需要再進行拼接。

生成簽名的過程如下:

1) 對拼接的引數字串S1通過SHA256演算法計算摘要,得到字串S2;
2) 對字串S2使用私鑰證照進行加密,並進行base64轉碼,得到簽名字串sign; 接收方收到報文後先進行base64解碼,再使用公鑰證照解密,然後驗證簽名的合法性。

注意事項:

1) 空引數不參與簽名;
2) 引數列表中的sign欄位不參與簽名;
3) 為了簡化處理,<xml>標籤也參與簽名;
4) 引數區分大小寫;
5) RSA加密的規則為:由交易發起方進行私鑰加密,接收方進行公鑰解密;(可以使用RSA公私鑰校驗工具來校驗商戶RSA公私鑰是否匹配)
6) 系統會對商戶公鑰證照的有效性進行校驗。

簽名程式碼:


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裡邊還有申請退款,申請撤單的介面,其實寫好一個介面的完成流程,別的流程都是直接套用就可以了。

部落格地址

相關文章