建议 | 在中国不是程序猿,不建议你用序列化!

Hi!我是小小,今天是本周的第四篇,本篇将会着重讲解关于Java序列化的内容

Java序列化

java序列化和反序列化数据,是通过ObjectOutputStream和ObjectInputStream这两个类来实现的,

举个例子:

要序列化的对象data1

public class data1 implements Serializable {
private int id;
private String name;
private String pwd;
private String pwd2;
public int getId(){ return id; }
public void setId(int id){ this.id = id; }
public String getName(){ return name; }
public void setName(String name){ this.name = name; }
public String getPwd(){ return pwd; }
public void setPwd(String pwd){ this.pwd = pwd; }
public String getPwd2(){ return pwd2; }
public void setPwd2(String pwd2){ this.pwd2 = pwd2; }
}

序列化操作类SerializeTest

public class SerializeTest {
public void serialize() throws Exception{
data1 d = new data1();
d.setId(1036);
d.setName("data1");
d.setPwd("pwd1");
d.setPwd2("pwd2");
FileOutputStream fos = new FileOutputStream("d:/project/serial/data1");
ObjectOutputStream oos = new ObjectOutputStream(fos); //创建Object输出流对象
oos.writeObject(d); //向data1文件中写入序列化数据data1类
fos.close();
oos.close();
System.out.println("序列化完成");
}
public data1 deSerialize() throws Exception{
FileInputStream fis = new FileInputStream("d:/project/serial/data1");
ObjectInputStream ois = new ObjectInputStream(fis); //创建Object输入流对象
data1 d = (data1)ois.readObject(); //从data1文件中反序列化出data1类数据
ois.close();
fis.close();
return d;
}
public static void main(String[] args) throws Exception{
SerializeTest s = new SerializeTest();
s.serialize();
data1 d = s.deSerialize();
System.out.println("id:"+d.getId());
System.out.println("name:"+d.getName());
System.out.println("pwd:"+d.getPwd());
}
}

执行后会发现 序列化成功,输出文件data1,同时反序列化成功,我们可以从data1文件中反序列化出data1类,能够获取其中的信息。

我们看看data1文件, notepad打开它长这样

再按十六进制打开看看,

是的,这就是序列化。

Java序列化缺陷

无法跨语言

Java序列化目前只支持Java语言实现的框架,其它语言大部分都没有使用Java的序列化框架,也没有实现Java序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用Java序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化。

容易被攻击

Java官网安全编码指导方针里有说明,“对于不信任数据的反序列化,从本质上来说是危险的,应该避免“。可见Java序列化并不是安全的。

我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。

对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。

Set root = new HashSet();
Set s1 = root;
Set s2 = new HashSet();
for (int i = 0; i < 100; i++) {
Set t1 = new HashSet();
Set t2 = new HashSet();
t1.add("test"); //使t2不等于t1
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}

实现攻击的原理:Apache Commons Collections允许链式的任意的类函数反射调用,攻击者通过实现了Java序列化协议的端口,把攻击代码上传到服务器上,再由Apache Commons Collections里的TransformedMap来执行。

序列化后的流太大

序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,那么这种序列化机制实现的二进制编码完成的二进制数组大小,相比于 NIO 中的 ByteBuffer 实现的二进制编码完成的数组大小,有没有区别呢?

User user = new User();
user.setUserName("test");
user.setPassword("test");
ByteArrayOutputStream os =new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(os);
out.writeObject(user);
byte[] testByte = os.toByteArray();
System.out.print("ObjectOutputStream 字节编码长度:" + testByte.length + "n");
ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);
byte[] userName = user.getUserName().getBytes();
byte[] password = user.getPassword().getBytes();
byteBuffer.putInt(userName.length);
byteBuffer.put(userName);
byteBuffer.putInt(password.length);
byteBuffer.put(password);
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
System.out.print("ByteBuffer 字节编码长度:" + bytes.length+ "n");

结果

ObjectOutputStream 字节编码长度:99
ByteBuffer 字节编码长度:16

性能太差

如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。

User user = new User();
user.setUserName("test");
user.setPassword("test");
long startTime = System.currentTimeMillis();
for(int i=0; i<1000; i++) {
ByteArrayOutputStream os =new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(os);
out.writeObject(user);
out.flush();
out.close();
byte[] testByte = os.toByteArray();
os.close();
}
long endTime = System.currentTimeMillis();
System.out.print("ObjectOutputStream 序列化时间:" + (endTime - startTime) + "n");
long startTime1 = System.currentTimeMillis();
for(int i=0; i<1000; i++) {
ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);
byte[] userName = user.getUserName().getBytes();
byte[] password = user.getPassword().getBytes();
byteBuffer.putInt(userName.length);
byteBuffer.put(userName);
byteBuffer.putInt(password.length);
byteBuffer.put(password);
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
}
long endTime1 = System.currentTimeMillis();
System.out.print("ByteBuffer 序列化时间:" + (endTime1 - startTime1)+ "n");

运行结果

ObjectOutputStream 序列化时间:29
ByteBuffer 序列化时间:6

总结

  1. Java 默认的序列化是通过 Serializable 接口实现的,只要类实现了该接口,同时生成一个默认的版本号,就可以实现序列化
  2. 序列化存在存在安全漏洞、不跨语言以及性能差等缺陷,
  3. FastJson、Protobuf、Kryo 是比较有特点的,而且性能以及安全方面都得到了业界的认可,我们可以结合自身业务来选择一种适合的序列化框架

关于作者

我是小小,双鱼座的程序猿,我们下期再见~bye~

mySoul
我还没有学会写个人说明!
上一篇

343官方人员否认取消Xbox One版《光环:无限》

下一篇

研究称记忆B细胞或有助于新冠幸存者产生更持久的免疫力

你也可能喜欢

评论已经被关闭。

插入图片