最近遇到一个bug很有意思,java客户端加密后传给服务端,服务端是GO Listen请求后解密处理,但是服务端解密失败。排查过程如下:
1.首先请求过程中有中间服务器转发,因此猜想可能是转发数据错误。由于发送数据为二进制流,因此对比客户端发送时与服务端接收到的数据的md5和base64,一致,排除中间传输错误。
2.再猜想是不是加密解密方法设置不一样导致失败。因为加密解密用的RSA,网上资料显示不同的RSA padding方式以及不同的provider都可能导致加密解密不一致从而解密失败,因此保证都是RSA/ECB/PKCS1Padding填充方式,而且java端两个provider(sun oracle实现和Bouncy Castle实现)都尝试下,还是有问题。
3.这时候没办法了,只能跟踪对比数据流了。在客户端发送二进制时计算数据md5和base64,分别发送二进制和base64数据,然后服务端收到二进制和base64,此时对二进制解密错误,但是对base64数据解码后再解密发现正确。这是为什么呢?对比收到的二进制数据和base64解码后的二进制,发现前者相对后者多了很多239,191,189,这些正是UTF8未识别字符的编码,因此问题就是客户端转二进制流时导致的问题。
下面用代码复现这一过程,
public static void EncodeAndDecode(String charset) throws UnsupportedEncodingException {
System.out.println("\r\n encode and decode with charset:" + charset);
byte[] bytes = new byte[]{80, 75, 3, 4, 10, 60, 82, -83, 68, 8, 0, 28, 0, 80, 97, 121, 108, 108};
System.out.println(Arrays.toString(bytes));
//先转成字符串
String s = new String(bytes, 0, bytes.length, charset);
//System.out.println(s);
//字符串再反转成字节
byte[] bytes2 = s.getBytes(charset);
System.out.println(Arrays.toString(bytes2));
}
UTF8的编码规则如下:
Unicode编码(16进制) UTF-8 字节流(二进制)000000 – 00007F 0xxxxxxx000080 – 0007FF 110xxxxx 10xxxxxx000800 – 00FFFF 1110xxxx 10xxxxxx 10xxxxxx010000 – 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
转码规则如下:
1.以0,110,1110,11110开头的字节,可以被识别
2.如果以10开头,则要求前一个字节必须以10,110,1110,11110开头
3.如果以10开头且前一个也以10开头,则再前一个必须以10,1110,11110开头
4.如果以10开头且前一个以10开头且再前一个也以10开头,则再前一个必须以11110开头
5.不能被识别的字节,每一个会转换成三个字节{11101111,10111111,10111101},也就是239,191,189
因此,实际编码中先将ASCLL二进制流转成字符串,如果这里是按照GBK编码转换再转回是没任何问题的。但是如果按照UTF-8转换时成字符串时,-83无法识别,会转成字节-17, -65, -67(其实就是239,191,189,因为java中byte-128~127)。
错误演示代码下载链接