支付與簽名原串的那些事,但選擇排序生成簽名原串

引題

【備註】簽名原串的源碼放在git上了,請你們參看:項目源碼前端

筆者最近在作支付、調用天貓優惠券、綁定銀行卡相關的業務,在這些業務中,咱們都須要將數據加密。然而,數據的加密方式不一樣,綁定銀行卡用md5加密,這不涉及金錢上的往來,使用MD5加密沒問題。然而,一旦涉及了金錢,好比支付業務,那麼,這種方式並很差。由於黑客頗有可能截取報文,修改密碼後盜取金額,於是,咱們採用RSA的加密方式。這裏以連連支付爲講解示例。java

講解連連支付以前,須要介紹非對稱加密算法。git

非對稱加密

咱們在經過ip傳輸數據時,若是採用對稱加密,即一個主站和用戶之間可使用相同的密鑰對傳輸內容進行加密,主站和用戶之間是知道彼此的密鑰。然而,ip報文就比如在官道上運輸糧草、黃金、物資,雖然相對來講比較安全,但很容易被人盯上。密鑰自己若是被盜,那麼,再複雜的密鑰也無濟於事。天然的想法是在密鑰上再加密,這就是遞歸的窮舉問題了。算法

這並非最好的辦法,有沒有一種方式,即報文被截取以後,黑客依然機關用盡。這就出現了一種全新的算法,即RSA加密算法。它把密碼革命性地分紅公鑰和私鑰,因爲兩個密鑰並不相同。apache

  • 首先經過openssl genrsa -out rsa_private_key.pem 1028 生成pkcs1格式的1028個字節的私鑰(適合PHP等前端),即:

MIICXgIBAAKBgQsyeT57L81ie1Lm1hEb7RVa9JszkhmuNAu7garMbmHInXRJBkqj
GWMqRFp0KQWYGGRYRqG59XVXYub3KuTE/9FamifG+d+EyUNFbwcG9H1g+kSnm868
MhBp1wr2zec/s47Bbx0fbtRYPXeQrkdzz6oAxVLoNDp+7eRixvlTe6c0LwIDAQAB
AoGBCx+1vBD9yHlSM2YIvS6VNmYKJDXzq3eZVR6PD3PRJWv8oQ37JiMqkY3oIkTM
jDYx5V6drQXliRGru/FJt8TOsNM7nmu1sGQH2Ae6WPHnqWHDJpSlEQ/rSzAv4XYx
WZtYWq/6ToT25foJ7e+BL2uMKKAq/64deiLt+K7hQWUi6nTBAkEDlqt/j/cYEGnT
eY2GBRTbLLLJGZ+c3hSHSS84n82l0U2qnNA3zrxshZc7hU6NTPrrQzmjIl0MGimP
VbDNwC59qQJBAx7IQx6ec1OoNA+chz1Xh/ipklcximKdPNW6QByEZ8B6lp74l2SJ
aISeqe+WCHvnk6FVpOTqC3rWmQWsVje42hcCQQGOZL9EKq8X5xzbuOEm8P1/q+UE
JLD9qj9lIIJY4vEHDLxxluas1A/n+0bHr+IdQS+njqZNb7ag3ecYDT2dG0xJAkB6
Fv/zUSKtebsjW7hsDtHwlvKQMzlEo2XmAQbFlRNKnzIgcDyrmDkKdDnjLdp0Hcw5
z55ZgtBoYR6YeGPhNnbXAkEC/hvl31bulAqTGdZsVYY6FEVn9TXbsF9mTFSyFbGH
XjjILiDu9dQasPVBP5vLNt+ClGJJJ36ffVaX7FSbHVs7iA==json

  • 然而,咱們後臺使用的是Java,須要將其轉爲pkcs8格式的私鑰,即:

MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBCzJ5PnsvzWJ7UubW
ERvtFVr0mzOSGa40C7uBqsxuYciddEkGSqMZYypEWnQpBZgYZFhGobn1dVdi5vcq
5MT/0VqaJ8b534TJQ0VvBwb0fWD6RKebzrwyEGnXCvbN5z+zjsFvHR9u1Fg9d5Cu
R3PPqgDFUug0On7t5GLG+VN7pzQvAgMBAAECgYELH7W8EP3IeVIzZgi9LpU2Zgok
NfOrd5lVHo8Pc9Ela/yhDfsmIyqRjegiRMyMNjHlXp2tBeWJEau78Um3xM6w0zue
a7WwZAfYB7pY8eepYcMmlKURD+tLMC/hdjFZm1har/pOhPbl+gnt74Eva4wooCr/
rh16Iu34ruFBZSLqdMECQQOWq3+P9xgQadN5jYYFFNsssskZn5zeFIdJLzifzaXR
Taqc0DfOvGyFlzuFTo1M+utDOaMiXQwaKY9VsM3ALn2pAkEDHshDHp5zU6g0D5yH
PVeH+KmSVzGKYp081bpAHIRnwHqWnviXZIlohJ6p75YIe+eToVWk5OoLetaZBaxW
N7jaFwJBAY5kv0QqrxfnHNu44Sbw/X+r5QQksP2qP2Ugglji8QcMvHGW5qzUD+f7
Rsev4h1BL6eOpk1vtqDd5xgNPZ0bTEkCQHoW//NRIq15uyNbuGwO0fCW8pAzOUSj
ZeYBBsWVE0qfMiBwPKuYOQp0OeMt2nQdzDnPnlmC0GhhHph4Y+E2dtcCQQL+G+Xf
Vu6UCpMZ1mxVhjoURWf1NduwX2ZMVLIVsYdeOMguIO711Bqw9UE/m8s234KUYkkn
fp99VpfsVJsdWzuIapi

  • 咱們將pkcs8格式的私鑰轉化爲公鑰,即

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQsyeT57L81ie1Lm1hEb7RVa9Jsz
khmuNAu7garMbmHInXRJBkqjGWMqRFp0KQWYGGRYRqG59XVXYub3KuTE/9FamifG
+d+EyUNFbwcG9H1g+kSnm868MhBp1wr2zec/s47Bbx0fbtRYPXeQrkdzz6oAxVLo
NDp+7eRixvlTe6c0LwIDAQAB數組

你會發現,不管是pkcs1的私鑰,仍是pkcs8格式的私鑰,其與公鑰並不相等。由於, 這就是所謂的非對稱加密。私鑰是用來對公鑰加密信息解密的,須要保密。而公鑰是對信息進行加密,任何人均可以知道,包括hack。咱們在傳輸的時候,雙方都遵照這個契約:安全

  • 甲該訴乙,使用RSA算法進行加密,乙說,好的。
  • 甲和乙分別根據RSA生成一對密鑰,互相發送公鑰。
  • 甲使用乙的公鑰給乙加密報文信息。
  • 乙收到信息,並用本身的密鑰進行解密。
  • 乙使用一樣的方式給甲發送消息,甲使用相同方式進行解密。

其實,咱們在使用連連支付時,也遵照這個規則。咱們首先生成一對公私鑰。將生成的公鑰上傳到連連商戶站的後臺,連連那邊就接收到了咱們的公鑰。咱們再從連連商戶站的後臺下載連連公鑰,咱們將私鑰和簽名原串共同加密生成簽名,這就是加簽。加簽後的數據和連連公鑰再次加密,經過HttpClient調用連連支付的接口,將加簽後的信息傳遞給連連。連連驗籤經過後,給咱們回傳他們加簽後的簽名信息,咱們這邊進行驗籤。這樣的加密方式是比較安全的。app

上面提到了兩次加密和簽名原串,那麼,簽名原串究竟是什麼?

簽名原串、加簽

咱們調用連連支付時,確定涉及到金額,商戶號,簽名方式,銀行卡名稱的。這些就是支付請求對象,假設,咱們如今有一個請求支付的javabean類:

/**
 * 這是支付父類的bean
 */
public class BaseRequestBean {

    private String oid_partner;

    private String sign;

    private String sign_type;

}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentRequestBean extends BaseRequestBean {

    private String api_version;

    private String card_no;

    private String flag_card;

    private String notify_url;

    private String no_order;

    private String dt_order;

    public String money_order;

    private String acct_name;

    private String bank_name;

    private String info_order;

    private String memo;

    private String brabank_name;
}

在上面的父類中有一個sign屬性,這裏存儲的是簽名原串加密後的數據。

  • 什麼是簽名原串?

即上面各個屬性(但不包含sign屬性)的值,按照必定格式,拼接而成的字符串。

  • 爲何除去sign屬性?

sign屬性存儲的將簽名原串加密後的字符串。

咱們首先要講支付請求對象賦值,如圖所示:
賦值後的支付請求對象

咱們經過一系列的操做,將其轉變爲以下格式的字符串,按照首字母由低到高的方式排名,若是首字母相同,再比較第二個,以此類推。。。具體怎麼生成的,下面會提到。

acct_name=jack&api_version=1.2&bank_name=工商銀行&brabank_name=中國工商銀行&card_no=123456677756&dt_order=20190302023423&flag_card=1212121&info_order=提現支付&memo=ceshi&money_order=12.00&notify_url= https://域名/項目名/接口&no_o...
  • 咱們第一次使用支付請求對象,是爲了將其生成簽名原串。簽名原串和咱們生成的pkcs8格式的私鑰加簽,第一次加密(加簽)涉及到咱們本身生成的私鑰,如代碼所示:
/**
     * 簽名處理
     *
     * @param prikeyvalue:私鑰
     * @param sign_str:簽名原串
     * @return
     */
    public static String sign(String prikeyvalue, String sign_str) {
        try {
            //【1】獲取私鑰
            KeyFactory keyFactory = KeyFactory.getInstance(PaymentConstant.SIGN_TYPE);
            //將BASE64編碼的私鑰字符串進行解碼
            BASE64Decoder decoder = new BASE64Decoder();
            byte[] encodeByte = decoder.decodeBuffer(prikeyvalue);
            //生成私鑰對象
            PrivateKey privatekey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodeByte));
            //【2】使用私鑰
            // 獲取Signature實例,指定簽名算法(本例使用SHA1WithRSA)
            Signature signature = Signature.getInstance(PaymentConstant.MD5_WITH_RSA);
            //加載私鑰
            signature.initSign(privatekey);
            //更新待簽名的數據
            signature.update(sign_str.getBytes(BaseConstant.CHARSET));
            //進行簽名
            byte[] signed = signature.sign();
            //將加密後的字節數組,轉換成BASE64編碼的字符串,做爲最終的簽名數據
            return new String(org.apache.commons.codec.binary.Base64.encodeBase64(signed));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
  • 咱們將加簽後的數據放置在請求對象的sign中,如圖所示

獲取加簽後的數據

  1. 咱們第二次使用支付請求對象,此次對象中的sign已經存值。咱們此時能夠將加簽後的請求對象和連連公鑰共同加密。此次涉及到的是咱們從商戶站下載下來的連連公鑰。調用連連的支付接口,如圖所示:

加簽後的支付請求對象和公鑰共同加密

書寫簽名原串

咱們上面一直在提簽名原串,其實怎麼生成的呢,我採用的是選擇排序算法,如代碼所示:

public static void main(String[] args) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("oid_partner", "12121212121");
        jsonObject.put("api_version", "1.2");
        jsonObject.put("sign_type", "rsa");
        jsonObject.put("flag_card", "1212121");
        jsonObject.put("notify_url", "https://域名/項目名/接口");
        jsonObject.put("no_order", "20190302023423zby");
        jsonObject.put("dt_order", "20190302023423");
        jsonObject.put("money_order", "12.00");
        jsonObject.put("card_no", "123456677756");
        jsonObject.put("acct_name", "jack");
        jsonObject.put("bank_name", "工商銀行");
        jsonObject.put("info_order", "提現支付");
        jsonObject.put("memo", "ceshi");
        jsonObject.put("brabank_name", "中國工商銀行");
        System.out.println(concatString(jsonObject,null));
    }

    /**
     * Created By zby on 15:07 2019/3/6
     * 拼接字符串
     */
    public static String concatString(JSONObject jsonObject, String type) {
        List<String> keys = keysSort(jsonObject);
        if (null == keys && keys.size() <= 0) {
            return null;
        }
        if (StringUtils.isBlank(type)) {
            type = "&";
        }
        StringBuilder concatBuilder = new StringBuilder();
        for (String key : keys) {
            concatBuilder.append(key + "=" + jsonObject.getString(key) + type);
        }
        return StringUtils.substring(concatBuilder.toString(), 0, concatBuilder.length() - 1);
    }


    /**
     * Created By zby on 14:55 2019/3/6
     * 獲取排序後的值
     */
    public static List<String> keysSort(JSONObject jsonObject) {
        if (null == jsonObject && jsonObject.size() <= 0) {
            return null;
        }
        List<String> keyList = new ArrayList<>(jsonObject.keySet());
        if (null != keyList && keyList.size() > 0) {
            for (int i = 0; i < keyList.size() - 1; i++) {
                for (int j = 0; j < keyList.size() - (i + 1); j++) {
                    String currKey = keyList.get(j);
                    String afterKey = keyList.get(j + 1);
                    if (StringUtils.isBlank(currKey) && StringUtils.isBlank(afterKey)) {
                        throw new RuntimeException("當前值爲空currKey=" + currKey + ",或者下一個值afterKey=" + afterKey);
                    }
                    char[] currKeyChars = currKey.toCharArray();
                    for (int k = 0; k < currKeyChars.length; k++) {
                        //保證當前字符是有效字符,即在26個字母之中,不在,直接放到後面
                        if (validateLetter(currKeyChars[k])) {
                            // 小於,不用排序,直接跳出
                            if (currKeyChars[k] < afterKey.charAt(k)) {
                                break;
                                //  等於,跳過此循環
                            } else if (currKeyChars[k] == afterKey.charAt(k)) {
                                continue;
                                //  大於,看清而定
                            } else {
                                if (validateLetter(afterKey.charAt(k))) {
                                    keyList.set(j, afterKey);
                                    keyList.set(j + 1, currKey);
                                }
                                break;
                            }
                        } else {
                            keyList.set(j, afterKey);
                            keyList.set(j + 1, currKey);
                            break;
                        }
                    }
                }
            }
        }
        return keyList;
    }

    /**
     * Created By zby on 14:52 2019/3/6
     * 驗證字符
     */
    public static boolean validateLetter(Character c) {
        if (c == null) {
            return false;
        }
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    }

生成結果爲:
acct_name=jack&api_version=1.2&bank_name=工商銀行&brabank_name=中國工商銀行&card_no=123456677756&dt_order=20190302023423&flag_card=1212121&info_order=提現支付&memo=ceshi&money_order=12.00&notify_url=https://域名/項目名/接口&no_o...

總結

支付並不複雜,說白了,無非是兩次加密。

第一次加密是將不包含sign屬性值的支付請求對象封裝的簽名原串和咱們生成的私鑰共同加密成簽名字符串,放進支付請求對象中的sign屬性中。

第二次加密是咱們使用連連支付的加密算法,將第一次加密的後支付請求對象和連連公鑰共同加密,封裝爲pay_load,調用連連支付的的接口請求支付。

相關文章
相關標籤/搜索