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

念兮

引题

【备注】签名原串的源码放在git上了,请大家参看:项目源码

笔者最近在做支付、调用天猫优惠券、绑定银行卡相关的业务,在这些业务中,我们都需要将数据加密。然而,数据的加密方式不同,绑定银行卡用md5加密,这不涉及金钱上的往来,使用MD5加密没问题。然而,一旦涉及了金钱,比如支付业务,那么,这种方式并不好。因为黑客很有可能截取报文,修改密码后盗取金额,因而,我们采用RSA的加密方式。这里以连连支付为讲解示例。

讲解连连支付之前,需要介绍非对称加密算法。

非对称加密

我们在通过ip传输数据时,如果采用对称加密,即一个主站和用户之间可以使用相同的密钥对传输内容进行加密,主站和用户之间是知道彼此的密钥。然而,ip报文就好比在官道上运输粮草、黄金、物资,虽然相对来说比较安全,但很容易被人盯上。密钥本身如果被盗,那么,再复杂的密钥也无济于事。自然的想法是在密钥上再加密,这就是递归的穷举问题了。

这并不是最好的办法,有没有一种方式,即报文被截取之后,黑客依然无计可施。这就出现了一种全新的算法,即RSA加密算法。它把密码革命性地分成公钥和私钥,由于两个密钥并不相同。

  • 首先通过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==

  • 然而,我们后台使用的是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
fp99VpfsVJsdWzuI

  • 我们将pkcs8格式的私钥转化为公钥,即

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

你会发现,不论是pkcs1的私钥,还是pkcs8格式的私钥,其与公钥并不相等。因为, 这就是所谓的非对称加密。私钥是用来对公钥加密信息解密的,需要保密。而公钥是对信息进行加密,任何人都可以知道,包括hack。我们在传输的时候,双方都遵守这个契约:

  • 甲该诉乙,使用RSA算法进行加密,乙说,好的。
  • 甲和乙分别根据RSA生成一对密钥,互相发送公钥。
  • 甲使用乙的公钥给乙加密报文信息。
  • 乙收到信息,并用自己的密钥进行解密。
  • 乙使用同样的方式给甲发送消息,甲使用相同方式进行解密。

其实,我们在使用连连支付时,也遵守这个规则。我们首先生成一对公私钥。将生成的公钥上传到连连商户站的后台,连连那边就接收到了我们的公钥。我们再从连连商户站的后台下载连连公钥,我们将私钥和签名原串共同加密生成签名,这就是加签。加签后的数据和连连公钥再次加密,通过HttpClient调用连连支付的接口,将加签后的信息传递给连连。连连验签通过后,给我们回传他们加签后的签名信息,我们这边进行验签。这样的加密方式是比较安全的。

上面提到了两次加密和签名原串,那么,签名原串到底是什么?

签名原串、加签

我们调用连连支付时,肯定涉及到金额,商户号,签名方式,银行卡名称的。这些就是支付请求对象,假设,我们现在有一个请求支付的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,调用连连支付的的接口请求支付。

阅读 1.4k

企业级开发讲解
我们在企业开发过程中,每个项目都会有所收获。如果将收获写成一篇篇文章,可以成为他人的指路明灯,也...
46 声望
6 粉丝
0 条评论
你知道吗?

46 声望
6 粉丝
宣传栏