玩轉京東支付(python)

說明

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

必定要仔細的看這些參數的說明數組

特殊參數說明以下:安全

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

附帶一下博客地址

相關文章
相關標籤/搜索