对接第三方服务引起的小思考-回调和Sign算法

背景

​ 最近在对接一个同事写的支付公用模块,然后对第三方服务引起一两个小思考。

思考

回调

来看看我们同事是如何做回调的。

首先,请求支付接口的时候,将回调URL作为请求body的一个参数[不加密]。

{
    "xxx": "xxx",
    "xxx": "xx",
    "xxxxx": "xxxxx",
    "callBackUrl": "http://www.baidu.com"
}

然后,当第三方支付服务成功后,支付服务会对上面的回调URL发出一次http请求,然后固定请求体带上以下参数:

{
    "code": 0,
    "充值订单号": "3333333333333333",
    "支付订单号": 361504175005106176,
    "支付状态": "SUCCESS",
    "充值数额": 88.00,
    "subject": "游戏",
    "gmtCreate": "2019-08-21 10:49:21",
    "gmtPayment": "2019-08-21 10:49:21",
}

到这里,我们可以看到

1、回调url是没有加密的,那么如果有黑客拦截到此数据,就可以拿到此url不断地攻击服务器了。

2、回调时的参数是支付服务方固定的,而且没有做到扩展,调用方不能增加自定义参数。

那么比较好的解决方案有什么呢,我们可以参考一下阿里的对象存储OSS是怎么做的。
Callback
用户只需要在发送给 OSS 的请求中携带相应的 Callback 参数,即能实现回调。
Callback包含下面字段(json格式):

{
    "callbackUrl":"121.101.166.30/test.php",  // 回调url
    "callbackHost":"oss-cn-hangzhou.aliyuncs.com", // 回调host
    "callbackBody":"{"mimeType":${mimeType},"size":${size}}", //回调请求体
    "callbackBodyType":"application/json" // 回调请求体类型
}

​ 然后将Callback对象转成Json字符串,再进行Base64编码,最后将Base64编码后的字符串作为一个请求参数即可。最后的请求体可能为如下:

{
    "xxx": "xxx",
    "xxx": "xx",
    "xxxxx": "xxxxx",
    "callBack": "23jdf7adf8gfasg98g78a9dgda"
}

​ 这样的话,我们可以看到,这就算是被别人拦截了,第一眼看过去肯定是懵逼的。但是呢,只要猜到是base64编码的,反编码后还是可以看到里面的东西。如果对安全性的要求是极致的,大家还可以加上加密算法[可解密],千万不要加密完不能倒退回来。。。

Sign算法

​ 一般的Sign算法,将请求参数以key1=value2&key2=value2的格式拼接起来,然后再拼接header的参数,最后的格式为:hKey1=hValue2&hKey2=hValue2&data=xxxx,而data的值就是上面请求参数拼接后的字符串,最后进行加密。
​ 我们可以想象,如果客户端的拼接顺序和服务端拼接的顺序不一致,那么最后加密后的字符串肯定是不相等,那么最后必须是服务端返回一句话:签名不合法。
下面,我们看一下一开始SignUtil中对请求参数的拼接函数:

/**
     * 将参数进行拼接
     * 返回结果为: key1=value1&key2=value2&key3=value3
     *
     * @param map Map参数
     * @return String
     */
public static String mapToString(Map<String, Object> map) {
    StringBuffer builder = new StringBuffer();
    map.forEach((key, value) -> builder.append(key).append("=").append(value).append("&"));
    builder.deleteCharAt(builder.length() - 1);
    return builder.toString();
}

​ 我们可以看到,是遍历Map来进行拼接。Map如果以顺序分类,可以分为两大类:一种是有序(例如LinkedHashMap和TreeMap),一种是无序(HashMap)。如果支付服务提供的SignUtil提供的拼接方法如上,那么就是说,我们开发者并不知道支付服务端是传入哪种类型的Map来进行拼接;这时候如果支付服务端使用的是TreeMap,而我们自己在不知道的前提下使用了比较常用的HashMap,那么这时候就出大问题了,签名永远是错误的。

解决方法(支付服务方提供SignUtil):

1、第三方服务的开发文档必须提醒开发者该传入的Map类型。

​ 拼接方法传入参数还是可以为Map,因为利用了泛型这样比较通用,但是呢文档该注重提醒开发者是传入有序的还是无序的实现类。

2、拼接方法传入参数指定Map类型,甚至具体到实现类。

​ 虽然说这样没有使用到泛型,显得不够通用,可是正因为如此,能强制规定开发者的输入,能避免非常多不必要的坑。

/**
     * 将参数进行拼接
     * 返回结果为: key1=value1&key2=value2&key3=value3
     *
     * @param map HashMap参数
     * @return String
     */
public static String mapToString(HashMap<String, Object> map) {
    StringBuffer builder = new StringBuffer();
    map.forEach((key, value) -> builder.append(key).append("=").append(value).append("&"));
    builder.deleteCharAt(builder.length() - 1);
    return builder.toString();
}

总结

​ 如果是做提供第三方服务的程序猿,我们必须考虑到接口的通用性,还有更重要的是准确性;然后还有就是文档应该简洁而不简单,能让开发者看文档即可一次搞定对接,而不是浪费不必要的时间去尝试,去揣测第三方服务究竟是如何设计的。

原文地址:https://www.cnblogs.com/Howinfun/p/11612643.html