Java序列化与反序列化

Chiexf Lv4

0x00 概述

Java序列化是指把Java对象转换为字节序列的过程

Java反序列化是指把字节序列恢复为Java对象的过程

0x01 为什么需要序列化和反序列化

当两个Java进程进行通信时,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;接收方需要从字节序列中恢复出Java对象。

0x02 序列化和反序列化的作用

  • 数据持久化:

将对象转换为字节流后,可以将字节流保存到磁盘或数据库中,实现数据的持久化。在下次需要时,可以从存储介质中读取字节流并进行反序列化,重新得到原始对象。

  • 远程通信:

在分布式系统中,不同的计算节点之间需要进行数据的传输和共享。通过序列化和反序列化,可以将对象转换为可传输的字节流,在网络上进行传输,并在接收端反序列化为对象。

  • 缓存机制:

在缓存技术中,可以通过将对象序列化后存储在缓存中,下次需要时直接从缓存中读取并反序列化为对象,提高数据的读取效率。

  • 对象复制和深拷贝:

有时需要对对象进行复制或深拷贝,在内存中创建一个与原始对象完全相同的新对象。通过序列化和反序列化,可以实现对象的深度复制,即创建一个与原始对象相互独立的副本。

  • 分布式计算和集群:

在分布式计算和集群环境中,任务可以在不同的节点上并行执行。通过序列化和反序列化,可以将任务对象传输到具体的执行节点,以便执行远程调用。

0x03 序列化和反序列化的实现

实现序列化和反序列话的条件

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列,不然会抛出异常。

  • Serializable 接口

Serializable 是 Java 中的一个接口,它是一个标记接口,不包含任何方法。

1
2
public interface Serializable {
}

示例

  • TestPerson类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestPerson implements Serializable {
private String name;
private int age;

public TestPerson(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "TestPerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
  • 对TestPerson类进行序列化操作,并将生成的字节序列写入 ser.bin 文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestSerializable {
public static void serialize(Object obj) throws Exception {
// 创建一个 FileOutputStream 对象,用于写入字节流到文件
FileOutputStream fileOut = new FileOutputStream("ser.bin");
// 创建一个 ObjectOutputStream 对象,用于将对象转换为字节流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOut);
// 将 person 对象写入到 ObjectOutputStream 中,进行序列化
objectOutputStream.writeObject(obj);

// 关闭资源
objectOutputStream.close();
fileOut.close();

System.out.println("TestPerson 对象已经序列化到 person.ser 文件.");
}

public static void main(String[] args) throws Exception {
TestPerson testPerson = new TestPerson("kk", 12);
// System.out.println(testPerson);
serialize(testPerson);
}
}



/*
输出结果:
TestPerson{name='kk', age=12}
TestPerson 对象已经序列化到 ser.bin 文件.
*/
  • 从序列化的字节序列中恢复 TestPerson对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestUnserialize {
public static Object testUnserialize(String filename) throws Exception {
// 创建一个 FileInputStream 对象,用于从文件中读取字节流
FileInputStream fileInputStream = new FileInputStream("ser.bin");
// 创建一个 ObjectInputStream 对象,用于将字节流转换为对象
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

//从 ObjectInputStream 中读取字节流
Object obj = objectInputStream.readObject();

// 关闭资源
objectInputStream.close();
fileInputStream.close();

return obj;

}

public static void main(String[] args) throws Exception {
//将其反序列化为 Person 对象
TestPerson testPerson = (TestPerson) testUnserialize("ser.bin");
System.out.println(testPerson);
}
}
/*
输出结果:
TestPerson{name='kk', age=12}
*/

transient 修饰符

  • 当一个成员变量被 transient 修饰时,它将不会被默认的序列化机制序列化。
  • 当对象被序列化时,被 transient 修饰的字段会被忽略,并且在反序列化过程中会被赋予默认值(例如数值类型为0,引用类型为null)
  • transient 只对对象的序列化有效,并不影响对象的其他行为和方法调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestPerson implements Serializable {
// private String name;
private transient String name;

private int age;

public TestPerson(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "TestPerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
  • TestSerialize
1
2
3
输出结果:
TestPerson{name='kk', age=12}
TestPerson 对象已经序列化到 ser.bin 文件.
  • TestUnserialize
1
2
输出结果:
TestPerson{name='null', age=12}

0x04 为什么会产生安全问题

只要服务器反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。

可能存在的形式

  • 入口类的readObject直接调用危险方法。

HashMap<Object,Object>、Hashtable<Object,Object> ……

  • 入口类参数中包含可控类,该类有危险方法,readObject时调用
  • 入口类参数中包含可控类,该类又调用其他危险方法的类,readObject时调用

0x05 重写writeObject和readObject

开发者可以重写 writeObject和readObject 方法,这样在序列化/反序列化时,系统会调用自定义的 writeObject和readObject 方法而不是使用默认的方法。