今日推荐:Spring AI 再更新:如何借助全局参数实现智能数据库操作与个性化待办管理
文章链接:https://cloud.tencent.com/developer/article/2464797
推荐原因:深入探讨如何利用 Spring AI 的新功能,来构建一个智能化的个人助理系统,对Java开发者比较友好的一个AI解决方案。
在大数据时代,各种平台存储了大量的行为数据和用户信息,为了保证用户的隐私,数据安全作为数据治理的一部分,也被越来越多的人所提及。如何确保数据在传输过程中的机密性、成为了需要开发者需要考虑的难题。
最简单的方式就是在传输之前,使用加密算法对数据进行加密。数据加密作为一种有效的保护手段,已经被广泛应用于各种数据传输场景中。
数据加密是通过某种算法将明文数据转换为密文数据的一种技术。加密后的密文即使被非法获取,也无法直接读取其中的信息,只有通过正确的解密密钥才能还原成原始的明文数据。
所以数据加密的过程通常包括两个主要环节:加密和解密。加密过程使用加密算法将原始的明文数据通过特定的加密密钥转化为密文。常见的加密算法包括对称加密和非对称加密。
解密是将密文数据恢复成明文的过程,解密过程使用解密算法和密钥。如果加密算法是对称的,则加密和解密使用相同的密钥;如果是非对称的,则使用一对密钥,其中一个密钥用于加密,另一个密钥用于解密。
对称加密是最为常见的数据加密方式,指加密和解密使用相同的密钥。由于加密和解密使用相同的密钥,因此对称加密速度较快,适合大规模数据的加密。但其缺点也很明显,即密钥管理问题。一旦密钥被泄露,所有加密的数据都可能受到威胁。
常见的对称加密算法:
非对称加密算法使用一对密钥,其中一个用于加密(公钥),另一个用于解密(私钥) 。虽然公钥可以公开,但只有私钥持有者才能解密加密数据。因此,非对称加密在解决密钥分发和密钥管理问题上具有天然优势。
常见的非对称加密算法:
哈希算法是一种单向加密算法,用于生成固定长度的输出(哈希值),即使输入数据的长度发生变化。哈希算法通常用于验证数据完整性,广泛应用于数字签名和密码存储中。哈希算法不涉及解密过程,因此它不适用于保护数据机密性,但可以有效防止数据篡改。
常见的哈希算法:
在加解密的过程中,数据是以二进制的形式存在的。就拿对称加密AES来说,将要加密的字符串转换成二进制的字节数组,使用密钥加密之后再以字节数组的形式返回。
public static String encrypt(String data, String key) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedData = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedData);
}
但是对于加密后的二进制数据,用户是无法直接看到的,所以我们通过一些转换将二进制转换成可见字符,例如常见的base64、二进制转换成10进制/16进制等,这样我们就可以看到加密数据。
在解密过程中,是将加密后的二进制字节数组在解密成明文数据的字节数组。
public static String decrypt(String encryptedData, String key) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decodedData = Base64.getDecoder().decode(encryptedData);
byte[] decryptedData = cipher.doFinal(decodedData);
return new String(decryptedData);
}
然后我们通过构造String还原明文数据即可。我们对上面AES加解密的方法进行测试:
public static void main(String[] args) throws Exception {
String key = "1234567890123456";
String data = "Hello, aqi!";
String encryptedData = encrypt(data, key);
System.out.println("Encrypted: " + encryptedData);
String decryptedData = decrypt(encryptedData, key);
System.out.println("Decrypted: " + decryptedData);
}
运行输出:
那么,为什么加密后的二进制数据不用new String的形式,而是要转换成base64、16进制的形式呢?
因为String是将1个字节转换成ASCII码,而ASCII的最大值是127(第一位为符号位),也就是0111 1111,但是在加密的过程中,如果这个字节最高位是1,就会被转换为负数。因为在计算机中负数是以补码的形式存储的(取反加1)。
例如1111 1111,取反为加1为0000 0001,所以为-1。代码如下:
byte b = (byte)255;
System.out.println(b);
System.out.println(b & 0XFF);
运行结果:
而最后的 b & 0XFF 为什么可以输出255??0XFF是十六进制,转换成二进制是1111 1111,不应该也是-1吗?
(byte)255 变成 -1 是因为 Java 的 byte 类型是有符号的,而0xFF 是一个十六进制数,它本身表示的是 255,当作为无符号整数处理,直接解释为 255。所以b & 0XFF的规则是这样的:
b的-1转换成int类型,其会被表示为11111111 11111111 11111111 11111111,而在 & 过程中,OxFF要扩展成32位,因为是无符号数,所以使用零扩展,即00000000 00000000 00000000 11111111。根据 & 的规则,最后 b & 0XFF 结果为00000000 00000000 00000000 11111111,即255。
上面这个就是个扩展只是,加密后的二进制数据不用new String归根结底的原因就是无法保证字节在127之内,结果就是乱码,如图:
在数据传输过程中,数据加密可以确保数据的安全性,避免敏感信息被第三方窃取、篡改或伪造。例如HTTPS、VPN等。在大数据中的传输和存储过程中,可以通过上面提到的加密算法进行加解密。但是也需要考虑很多的问题:
密钥管理更多的体现在离线数据治理的场景中,而在数据传输的应用场景中,对性能(实时性)的要求会更高。所以在大数据中,如何在数据加密和实时性之间获得平衡也成为一个问题。所以现在更偏向与在传输中只对双方身份认证,而不对数据本加密,数据安全就放在数据治理的工作中。
那么不用加密算法可以实现数据传输的安全性吗??
抛开内网的环境下,谈谈我在数据传输中遇到的一个实时业务场景,白天大概16Gbytes/s +的流量。就是就是将字段、类型与字节数对应。听我细讲:
假如一条数据有3个字段,然后网络传输的过程中使用的是byte,最简单的方式就是使用getBytes将这条数据转换成字节,假如被人给获取了这条数据,使用new String就简单的破解了。
第一个方案就是指定每个字段的类型,然后对每个字段进行类型编码之后再进行传输。接下来我们看看如何对字段进行编码。
首先,需要数据传输方和接收方制定一个规范,将每个字段的名称与类型、字节数对应起来,例如:
其中string类型的处理比较复杂,因为不同长度的字符串字节数不一样,所以这里标了一个“变长”,通常情况下,可以在name前面增加一个name_length来name长度,后面讲到的TLV编码可以解决这个问题。
像age字段注意我们使用的是unsigned int的无符号类型,这样最高位就不是符号位了,就可以用2个字节的16个bit全部来表示数据内容,最后timestamp是long类型,除此之外还有4、5个字节的unsigned int以及double等类型,这里暂不讨论。
这里不讨论name的变长问题,这里就默认为3。先看传输方根据规范,如何将三个字段转换为字节数组。
图中的代码只是为了演示将每个字段按照类型处理成byte后,以网络序放入到了result字节数组中,在实际生产中会有更高效的编码方式。
不难发现,这一条数据一共用了13个字节,如果使用getBytes会占用多少个字节呢??
18个字节,其中还不包含两个分隔符的字节长度。所以这种传输方式可以减少带宽占用。
接收方收到数据后,就可以根据规范来解析每个字段。
其中,name是字符串类型,可以直接使用new String,age是将2个字节转换成short,timestamp是将8个字节转换成Long。在解析每个字段的过程中,我们需要根据规范中的字段长度,来移动下标获取字段对应的字节数。
可以查看上面解码中封装的方法,其中byteToUnsignedShort是2个字节转换成short,byteToLong是8个字节转换成long,这里都是比较基础的字节位运算。
public static int byteToUnsignedShort(byte[] b, int off) {
short s = 0;
short s0 = (short) (b[(1 + off)] & 0xFF);
short s1 = (short) (b[off] & 0xFF);
s1 = (short) (s1 << 8);
s = (short) (s0 | s1);
return s & 0xFFFF;
}
public static long byteToLong(byte[] b, int off) {
long s = 0L;
long s0 = b[(7 + off)] & 0xFF;
long s1 = b[(6 + off)] & 0xFF;
long s2 = b[(5 + off)] & 0xFF;
long s3 = b[(4 + off)] & 0xFF;
long s4 = b[(3 + off)] & 0xFF;
long s5 = b[(2 + off)] & 0xFF;
long s6 = b[(1 + off)] & 0xFF;
long s7 = b[off] & 0xFF;
s1 <<= 8;
s2 <<= 16;
s3 <<= 24;
s4 <<= 32;
s5 <<= 40;
s6 <<= 48;
s7 <<= 56;
s = s0 | s1 | s2 | s3 | s4 | s5 | s6 | s7;
return s;
}
假如在传输过程中,字节数组result被获取,没有规范的话使用new String是无法正确解析数据的。
如图,部分数据就会乱码,原因也是上面提到的ASCII的问题。
上面讲的字段类型编码,是比较常见的一种,数据中的每个字段按顺序排列,且所有字节都用来表示数据本身,这种编码方式我们称之为V(value)。除此之外,还有LV和TLV格式,L表示Length,所以LV通常来用来表示变长字段。
TLV(Type-Length-Value)是一种广泛应用于通信协议和数据存储的编码格式,用于表示数据的结构和内容。所以这里的一个每个字段是由三部分组成的:T 表示数据的类型(Type),L 表示数据的长度(Length),V 表示数据的值(Value)。
所以在规范制定的时候,对V编码格式要多出来一个TAG编号,用来作为每个字段的唯一标识。
在字段规范指定前,我们需要先制定TLV的规范。例如:T占用16bit(2字节),L占用8bit来表示V的长度。在实际开发中可以根据数据的长度等条件,来优化TLV规范。
例如我遇到的就是T占用12bit,Format占用4bit,Format是Length的枚举,当Format为0,Length为变长,Format为1时,Length=1,以此类推到6,当Format为7时,Length=8,Format为8时,Length=16,以此类推32/64/128。
这样TL的字节数又从24变成了16,这样意味着在解析TLV时,会多一些位运算,而V可以根据L的长度来调用对应的方法解析。
// Tag和format解析
int tagLow = bytes[off] & 0xFF;
int formatAndTagHigh = bytes[off + 1] & 0xFF;
int tag = ((formatAndTagHigh & 0x0F) << 8) | tagLow;
int format = (formatAndTagHigh >> 4) & 0X0F;
off += 2;
int length = 0;
// Value字段解析
switch (format) {
case 1:
length = 1;
bytes[off] & 0xff;
break;
case 2:
length = 2;
ConvToByte.byteToUnsignedShort(bytes, off);
break;
case 4:
ConvToByte.byteToUnsignedInt(bytes, off);
length = 4;
break;
case 7:
length = 8;
ConvToByte.byteToLong(bytes, off);
break;
}
TLV编码明显比之前讲的V编码麻烦好多,看起来也没有什么优势可言,为什么要使用TLV?
首先,V编码的字段是顺序排列的,需要按规范顺序移动下标顺序解析。解析完name的3个字节之后,紧挨着一定是表示age的两个字节,如果age为空,那么也要使用默认值将这个两个字节回填上,要不然就会解析错误。
而TLV就没有这个烦恼,每个字段是通过Tag和Length字段,来确定位置和长度,当解析出来Tag为0的时候,我们就将解析出来的Value放到集合中index=0的位置,当没有解析出来Tag为1的数据,我们就在使用集合中index=1的默认值即可,而不需要在数据中为字段填充默认值。
所以,TLV字段的随机分布使得数据传输更加安全,虽然TLV规范中的字段排列是name、age。但是编码成TLV格式的时候,可以编码成age、name进行传输,所所以TLV 格式的灵活性使其成为动态协议的理想选择。例如在需要时扩展新的字段时,可使用 TLV 格式实现向后兼容。
本篇文章刚开始是想讲常见的加密算法,然后就想起来了我遇到的数据传输中TLV的编码场景,最后就以文字的形式分享了出来。加密方案并不是一成不变,适合自己的实际业务才是最重要的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。