丛一次编码相关的坑说起

最近在项目上遇到一个编码相关的问题, 弄清楚过后觉得这个具备一定的典型意义,所以在这里记上。

出问题的代码大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
*jsonObject为一个格式为 {sign:xxx, tradeResponse: {}} 的JSON字符串
*
*/
String sign = jsonObject.getString("sign");
JSONObject tradeResponse = jsonObject.getJSONObject("tradeResponse");

try {
//获取到json
String jsonStr = tradeResponse.toString();
//调用系统属性配置获取密钥
return RSAUtils.verify(jsonStr.getBytes(), properties.getPubKey(), sign);

而加密的代码类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//真正的响应内容
Map tradeResponse = new HashMap(){{
put("success", true);
put("code", "0");
}};
String json = JSON.toJSONString(tradeResponse);
try {
//签名
String sign = RSAUtils.sign(json.getBytes(), privateKey);
Map result = new HashMap(){{
put("tradeResponse", tradeResponse);
put("sign", sign);
}};
String rs = JSON.toJSONString(result);
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}

这是一段经过简化的对HTTP请求进行加密的方法,应该说很多应用都会有。实际实现思路相差不大。 这段代码看起来没有什么问题,开发的时候在本地也测试通过了,但是最终却是在不同的平台/IDE/环境下上面的验证签名方法结果不一样,有成功有失败。

于是我在本地测试, 把上面的代码从业务里面拖出来放到单元测试里,然后通过debug拿到签名过后的jsonObject,放到单元测试里用几乎完全一样的方法测试, 结果是验证可以通过。

然后我又从以下几个方面仔细排查了下:

  • 客户端和服务器加解密使用的库(RSAUtil)
  • 校验公钥/私钥对
  • 在服务器端解密客户端收到的json字符串

最终结果毫无疑问都是没有问题的,看来最终的问题还是在调用加解密这部分。 于是,断点打到RSAUtils.verify内部,一一和本地单元测试的对比,果然发现了问题: byte[] 数组的值不一样。

这里有两个问题:

  • 对于相同的字符串,在应用中与在单元测试(同一个IDE同时跑起来)中获取到的byte[]竟然不一样。
  • 开发环境可以通过验证,到了测试环境又不一致

其实到了这里心里就已经基本有数了,问题的根源在于服务器在对响应内容进行加密的时候,获取字符串的byte[]数组没有指定编码,使用了系统的默认编码; 客户端在对收到的响应字符串进行解密时, 也没有对字符串的byte[]进行编码,使用了系统的默认编码。 而这种依赖外部环境的代码出问题也就毫无疑问了,解决的思路也很简单,加解密都使用相同的utf-8编码。

这个问题让我想到了2点:

  • 在前面的12factor - spring cloud推荐的一个编程方法论简述这篇翻译里提到过的依赖管理。 一个设计良好、编码规范的app应该是完全独立于平台的,不能依赖任意外部环境的设置,比如编码,比如某个环境环境变量等等。在设计、编码、编码检查的时候应该尤其注意这种。上面的getBytes方法就是一个典型的依赖了外部环境,并且还是隐式依赖。 类似的,如果代码里某些方法依赖了文件系统utfs/ext3之类的,也很可能在某个时间点爆发BUG,并且难以排查。 所以在编码时需要尤其注意这种。

  • 对于使用的工具、环境的整体概念
    工欲善其事必先利其器,开发总会用到各种各样的工具或者类库。Java开发最好用的IDE毫无疑问就是idea,但是有多少人清楚IDEA里面的配置及其对工程的影响呢?
    往简单了说,IDEA可以设置的编码包括全局编码、项目编码以及BOM相关的; 还可以设置项目的默认JDK版本、字节码版本、编码等等,往复杂了说,启动一个项目可以配置的各个东西哪些可以在应用里做哪些可以放到外部等等。而项目通常会使用maven, gradle之类的构建工具,那么在IDEA里面运行某个项目时,字节码版本是由哪个决定的? 出问题了是否需要离开IDE单独测试项目情况? 如果某个开发没有更改IDE的编码而编辑了文件并且提交会造成什么后果?

这些问题都是很简单的问题,但是我想指出的是 背后 每个人都需要逐步建立起这种思想:
我对我使用到的东西大概清楚每个部分都是干啥的,起了什么作用,以及不同部分之间的相互影响范围
这个当然不限于开发/构建工具,比如对于maven/gradle在ide里的操作以及单独在命令行的操作; 比如tomcat\war\netty\servlet\jsp\angular\http\dubbo这些在一个应用里所属的层级; 比如前端\后端\cdn\nginx。不一定能够一清二楚每个东西的原理,但是在一个整体应用中他们每一个所属的位置及作用应该要清清楚楚。

拧螺丝谁都会拧,但是只会拧一个位置的螺丝和掌握了所有位置的螺丝还是有点区别的。共勉~