前言
商务合同、契约关系等丢需要合作双方签名之后才能生效,签名能够证明是本人授权的合作协议,如果被伪造了合作关系,则可以根据签名的笔迹判断出来。同样这个思路可以引入支付行业,支付机构收到支付报文之后能否判断出是合法机构上送的信息非常关键,如果被不法分子恶意上送支付或者转账信息到支付机构,则可能直接把消费者银行卡中的资金扣除,这是非常严重的安全漏洞。
签名是防止这种情况发生的有效办法之一。支付机构会根据签名来判断是不是合作的商户上送的信息。另外支付数据在网络上传输,很容易被黑客拦截并篡改,把篡改之后的支付信息上送给支付机构,同样会给消费者造成很严重的资金损失。黑客拦截并篡改,把篡改之后的支付信息上送给支付机构,同样会给小费者造成很严重的资金损失。黑客拦截并篡改信息之后给消费者和商户造成的损失如下图所示:
由上图可以看出,传输的数据一旦被黑客篡改,本来消费者只需要支付100元,商户应该收到100元,但是数据被篡改成了消费者需要支付10000元,黑客的账户中会收到1000元。那么,如何解决这个安全漏洞呢?这就引入了电子签名,常见的加签/验签算法有RSA、AES 和 MD5
等。
加签/验签算法原理
以 MD5
为例, MD5
算法的核心是利用 Hash 的不可逆性,被加密后的密文无法通过解密函数来得到明文,并且一旦明文被改变,加密后的密文也是完全不一样的。基于这个特性,支付机构就可以判断出明文是否被篡改了。比如带加签的报文如下所示:
{
"trade_no": "192376547821987234",
"mch_id": "180212110446000000",
"bank_card_no": "622********",
"name": "苏**",
"phone": "188********",
"expire_date": "2901",
"cvn2": "123",
"trade_amount": "100",
"currency_code": "CNY",
"sign": "C9F6D25A07EF1709EAA7F64CAAC131F"
}
如果修改其中的金额,那么支付机构收到报文之后,会根据约定的加签方式, 对数据重新加签,并与报文中的签名进行对比,如果与上送的签名不一致,就判定报文被篡改,直接返回错误信息,不予处理。
加签验签示例
代码语言:javascript复制public class MD5Util {
static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
static String MD5 = "MD5";
public static String sign(String data, String key) throws Exception{
// 得到明文的字节数组
byte[] btInput = (data key).getBytes();
// 创建一个提供信息摘要算法的的对象(MD5摘要算法)
MessageDigest messageDigest = MessageDigest.getInstance(MD5);
// 使用指定的字节更摘要
messageDigest.update(btInput);
// 得到二进制密文
byte[] encryptData = messageDigest.digest();
// 把密文转换成十六进制的字符串形式
return bytesToHex(encryptData);
}
public static String bytesToHex(byte[] bytes){
int k = 0;
char[] hexChars = new char[bytes.length*2];
for(int i=0; i<bytes.length; i ){
byte byte0 = bytes[i];
hexChars[k ] = hexDigits[byte0 >>> 4 & 0xf];
hexChars[k ] = hexDigits[byte0 & 0xf];
}
return new String(hexChars);
}
public static boolean verifySign(String data, String key, String sign) throws Exception {
// 调用加签方法,对比加签后的签名是否一致
String encryptData = sign(data, key);
if(encryptData.equals(sign)){
return true;
} else {
return false;
}
}
}
支付网关收到报文之后会按照约定对报文进行验签,验签通过后继续进行下一步,验签失败会直接返回错误信息,无需请求支付业务系统,为支付主业务系统减轻了压力。MD5
加签需要有一个秘钥, 服务端和客户端的秘钥需要一致,这也是对称加密算法的特性,所以支付机构需要把秘钥传输给入驻的商户。
涉及网络传输就会有安全漏洞,在传输过程中秘钥可能被截取,如何解决这个问题呢?我们可以对传输的秘钥做一层加密,使用 RSA
加密算法,把加密后是秘钥传输给商户。基于MD5
的特性,只有明文相同,MD5
加密后的密文就相同,于是黑客有可能通过撞库的方式来破解明文。这种方式可以通过加盐的方式解决。加盐就是向明文中加入随机数,再使用MD5
加密算法生成密文,这样即使明文相同,每次生成的密文也不同,这样也就加明显大了暴力破解的难度。整个时序图如下图所示:
需要注意的是,加签的时候需要约定加签的规则,才能保住服务端和客户端的签名是一致的。比如参数的顺序,否则服务端和客户端加签后的签名可能不一致。一般会按照 key 的 ASCII 码排序后再进行加签,Java 的 TreeMap
是根据 key 排序的,默认情况下是升序排列。参数之前的拼接方式也需要提前约定好,比如拼接方式为:
key1= value1&key2=value2......
以 TreeMap
为例,对报文排序的源码如下所示:
public static String sortMap(Map<String,?> maps){
TreeMap<String, Object> treeMap = new TreeMap<>();
for(Map.Entry<String, ?> map: maps.entrySet()){
Object value = map.getValue();
if(value!=null){
treeMap.put(map.getKey(), value);
}
}
// 遍历 treeMap 并生成签名data
Iterator<String> iterator = treeMap.keySet().iterator();
StringBuilder sortData = new StringBuilder();
while (iterator.hasNext()){
String key = iterator.next();
String value = (String) treeMap.get(key);
sortData.append(key).append("=").append(value);
if(iterator.hasNext()){
sortData.append("&");
}
}
return sortData.toString();
}
小结
随机 salt 的生成方式有很多种,既可以使用时间戳,也可以使用随机数。总结一下签名的规则:
(1)参数需要按照一定的规则排序,比如按照 ASCII 码从小到大排序;
(2) 参数值为空时不参与签名;
(3)为了增加安全系数,可以通过加盐的方式提升破解难度,盐值既可以是随机数也可以是时间戳;
(4)提供的接口可能会增加字段,验签时必须支持增加扩展字段;
(5)参数名区分大小写。