支付与签名原串的那些事,但选择排序生成签名原串

引题

【备注】签名原串的源码放在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,调用连连支付的的接口请求支付。

相关文章
相关标签/搜索