资讯专栏INFORMATION COLUMN

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

DC_er / 2572人阅读

摘要:工商银行中国工商银行提现支付域名项目名接口我们第一次使用支付请求对象,是为了将其生成签名原串。第一次加密是将不包含属性值的支付请求对象封装的签名原串和我们生成的私钥共同加密成签名字符串,放进支付请求对象中的属性中。

引题

【备注】签名原串的源码放在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类:

</>复制代码

  1. /**
  2. * 这是支付父类的bean
  3. */
  4. public class BaseRequestBean {
  5. private String oid_partner;
  6. private String sign;
  7. private String sign_type;
  8. }
  9. @Data
  10. @AllArgsConstructor
  11. @NoArgsConstructor
  12. public class PaymentRequestBean extends BaseRequestBean {
  13. private String api_version;
  14. private String card_no;
  15. private String flag_card;
  16. private String notify_url;
  17. private String no_order;
  18. private String dt_order;
  19. public String money_order;
  20. private String acct_name;
  21. private String bank_name;
  22. private String info_order;
  23. private String memo;
  24. private String brabank_name;
  25. }

在上面的父类中有一个sign属性,这里存储的是签名原串加密后的数据。

什么是签名原串?

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

为什么除去sign属性?

sign属性存储的将签名原串加密后的字符串。

我们首先要讲支付请求对象赋值,如图所示:

我们通过一系列的操作,将其转变为如下格式的字符串,按照首字母由低到高的方式排名,如果首字母相同,再比较第二个,以此类推。。。具体怎么生成的,下面会提到。

</>复制代码

  1. 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¬ify_url=https://域名/项目名/接口&no_o...

我们第一次使用支付请求对象,是为了将其生成签名原串。签名原串和我们生成的pkcs8格式的私钥加签,第一次加密(加签)涉及到我们自己生成的私钥,如代码所示:

</>复制代码

  1. /**
  2. * 签名处理
  3. *
  4. * @param prikeyvalue:私钥
  5. * @param sign_str:签名原串
  6. * @return
  7. */
  8. public static String sign(String prikeyvalue, String sign_str) {
  9. try {
  10. //【1】获取私钥
  11. KeyFactory keyFactory = KeyFactory.getInstance(PaymentConstant.SIGN_TYPE);
  12. //将BASE64编码的私钥字符串进行解码
  13. BASE64Decoder decoder = new BASE64Decoder();
  14. byte[] encodeByte = decoder.decodeBuffer(prikeyvalue);
  15. //生成私钥对象
  16. PrivateKey privatekey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodeByte));
  17. //【2】使用私钥
  18. // 获取Signature实例,指定签名算法(本例使用SHA1WithRSA)
  19. Signature signature = Signature.getInstance(PaymentConstant.MD5_WITH_RSA);
  20. //加载私钥
  21. signature.initSign(privatekey);
  22. //更新待签名的数据
  23. signature.update(sign_str.getBytes(BaseConstant.CHARSET));
  24. //进行签名
  25. byte[] signed = signature.sign();
  26. //将加密后的字节数组,转换成BASE64编码的字符串,作为最终的签名数据
  27. return new String(org.apache.commons.codec.binary.Base64.encodeBase64(signed));
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. return null;
  32. }

我们将加签后的数据放置在请求对象的sign中,如图所示

我们第二次使用支付请求对象,这次对象中的sign已经存值。我们此时可以将加签后的请求对象和连连公钥共同加密。这次涉及到的是我们从商户站下载下来的连连公钥。调用连连的支付接口,如图所示:

书写签名原串

我们上面一直在提签名原串,其实怎么生成的呢,我采用的是选择排序算法,如代码所示:

</>复制代码

  1. public static void main(String[] args) {
  2. JSONObject jsonObject = new JSONObject();
  3. jsonObject.put("oid_partner", "12121212121");
  4. jsonObject.put("api_version", "1.2");
  5. jsonObject.put("sign_type", "rsa");
  6. jsonObject.put("flag_card", "1212121");
  7. jsonObject.put("notify_url", "https://域名/项目名/接口");
  8. jsonObject.put("no_order", "20190302023423zby");
  9. jsonObject.put("dt_order", "20190302023423");
  10. jsonObject.put("money_order", "12.00");
  11. jsonObject.put("card_no", "123456677756");
  12. jsonObject.put("acct_name", "jack");
  13. jsonObject.put("bank_name", "工商银行");
  14. jsonObject.put("info_order", "提现支付");
  15. jsonObject.put("memo", "ceshi");
  16. jsonObject.put("brabank_name", "中国工商银行");
  17. System.out.println(concatString(jsonObject,null));
  18. }
  19. /**
  20. * Created By zby on 15:07 2019/3/6
  21. * 拼接字符串
  22. */
  23. public static String concatString(JSONObject jsonObject, String type) {
  24. List keys = keysSort(jsonObject);
  25. if (null == keys && keys.size() <= 0) {
  26. return null;
  27. }
  28. if (StringUtils.isBlank(type)) {
  29. type = "&";
  30. }
  31. StringBuilder concatBuilder = new StringBuilder();
  32. for (String key : keys) {
  33. concatBuilder.append(key + "=" + jsonObject.getString(key) + type);
  34. }
  35. return StringUtils.substring(concatBuilder.toString(), 0, concatBuilder.length() - 1);
  36. }
  37. /**
  38. * Created By zby on 14:55 2019/3/6
  39. * 获取排序后的值
  40. */
  41. public static List keysSort(JSONObject jsonObject) {
  42. if (null == jsonObject && jsonObject.size() <= 0) {
  43. return null;
  44. }
  45. List keyList = new ArrayList<>(jsonObject.keySet());
  46. if (null != keyList && keyList.size() > 0) {
  47. for (int i = 0; i < keyList.size() - 1; i++) {
  48. for (int j = 0; j < keyList.size() - (i + 1); j++) {
  49. String currKey = keyList.get(j);
  50. String afterKey = keyList.get(j + 1);
  51. if (StringUtils.isBlank(currKey) && StringUtils.isBlank(afterKey)) {
  52. throw new RuntimeException("当前值为空currKey=" + currKey + ",或者下一个值afterKey=" + afterKey);
  53. }
  54. char[] currKeyChars = currKey.toCharArray();
  55. for (int k = 0; k < currKeyChars.length; k++) {
  56. //保证当前字符是有效字符,即在26个字母之中,不在,直接放到后面
  57. if (validateLetter(currKeyChars[k])) {
  58. // 小于,不用排序,直接跳出
  59. if (currKeyChars[k] < afterKey.charAt(k)) {
  60. break;
  61. // 等于,跳过此循环
  62. } else if (currKeyChars[k] == afterKey.charAt(k)) {
  63. continue;
  64. // 大于,看清而定
  65. } else {
  66. if (validateLetter(afterKey.charAt(k))) {
  67. keyList.set(j, afterKey);
  68. keyList.set(j + 1, currKey);
  69. }
  70. break;
  71. }
  72. } else {
  73. keyList.set(j, afterKey);
  74. keyList.set(j + 1, currKey);
  75. break;
  76. }
  77. }
  78. }
  79. }
  80. }
  81. return keyList;
  82. }
  83. /**
  84. * Created By zby on 14:52 2019/3/6
  85. * 验证字符
  86. */
  87. public static boolean validateLetter(Character c) {
  88. if (c == null) {
  89. return false;
  90. }
  91. return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
  92. }

生成结果为:
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¬ify_url=https://域名/项目名/接口&no_o...

总结

支付并不复杂,说白了,无非是两次加密。

第一次加密是将不包含sign属性值的支付请求对象封装的签名原串和我们生成的私钥共同加密成签名字符串,放进支付请求对象中的sign属性中。

第二次加密是我们使用连连支付的加密算法,将第一次加密的后支付请求对象和连连公钥共同加密,封装为pay_load,调用连连支付的的接口请求支付。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73589.html

相关文章

  • 回眸曾经的项目,第三方支付相关,所带来的沟通问题

    摘要:钱可以存储在自己的余额中,这就相当于微信钱包,余额可以提现到银行卡的中。我们的第三方支付平台是连连支付,杭州的一家公司。私钥怎么加签每个公司的加签方式是不一样的,支付宝有支付宝的加签方式,微信有微信的加签方式。 导读 笔者在校期间,通过自学java。学校里也开过这门课,但是,讲的都是一些基础,比如java的表达式、基本类型、自定义类型等等。也都是很基础的东西,就连lambda表达式都没...

    kaka 评论0 收藏0
  • 第三方支付对接流程

    摘要:防止在传输过程中被截获破解。将上面参数组成的字符串加上安全校验码组成待签名的数据,安全校验码通过平台分配,假设安全校验码为,那计算的原串为伪代码只有两方内部保存,确保传递的参数没有被第三方篡改。 转载请注明出处 http://www.paraller.com 原文排版地址 点击跳转 第三方支付 1、简单加密 目的是为了保证上传的参数信息没有被篡改,主要分成三部分 接口参数 : 需...

    seasonley 评论0 收藏0
  • 最长回文子串——Manacher 算法

    摘要:问题定义最长回文子串问题给定一个字符串,求它的最长回文子串长度。可以采用动态规划,列举回文串的起点或者终点来解最长回文串问题,无需讨论串长度的奇偶性。 0. 问题定义 最长回文子串问题:给定一个字符串,求它的最长回文子串长度。 如果一个字符串正着读和反着读是一样的,那它就是回文串。下面是一些回文串的实例: 12321 a aba abba aaaa tatt...

    mingzhong 评论0 收藏0
  • javascript replace方法

    摘要:字符串方法还是比较强大的,做个笔记总结。美元符号连字符与正则表达式相匹配的子字符串。美元符号单引号位于匹配子字符串右侧的文本。否则,第至个参数对应为捕获组匹配项,倒数的两个参数为匹配下标,原串。函数的匹配返回值,作为每次的匹配替换值。 javascript字符串方法replace还是比较强大的,做个笔记总结。 第一个参数 replace的第一个参数为字符串或者正则表达式。第二个参数...

    codercao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<